a control panel for my server
at main 4.5 kB view raw
1import { createHash, randomBytes } from "crypto"; 2import { Context } from "hono"; 3import { getCookie, setCookie, deleteCookie } from "hono/cookie"; 4import { sign, verify } from "hono/jwt"; 5 6const INDIKO_URL = process.env.INDIKO_URL || "https://indiko.dunkirk.sh"; 7const ORIGIN = process.env.ORIGIN || "http://localhost:3010"; 8const CLIENT_ID = process.env.CLIENT_ID || `${ORIGIN}/`; 9const CLIENT_SECRET = process.env.CLIENT_SECRET; 10const REDIRECT_URI = process.env.REDIRECT_URI || `${ORIGIN}/auth/callback`; 11const SESSION_SECRET = process.env.SESSION_SECRET || "development-secret-change-me"; 12const REQUIRED_ROLE = process.env.REQUIRED_ROLE; 13const IS_DEV = process.env.NODE_ENV !== "production"; 14 15export interface SessionPayload { 16 sub: string; 17 name: string; 18 role?: string; 19 exp: number; 20 [key: string]: unknown; 21} 22 23export interface PKCEChallenge { 24 verifier: string; 25 challenge: string; 26} 27 28export function generatePKCE(): PKCEChallenge { 29 const verifier = randomBytes(32).toString("base64url"); 30 const challenge = createHash("sha256").update(verifier).digest("base64url"); 31 return { verifier, challenge }; 32} 33 34export function generateState(): string { 35 return randomBytes(16).toString("base64url"); 36} 37 38export function getAuthorizationUrl(pkce: PKCEChallenge, state: string): string { 39 const params = new URLSearchParams({ 40 response_type: "code", 41 client_id: CLIENT_ID, 42 redirect_uri: REDIRECT_URI, 43 scope: "profile email", 44 state, 45 code_challenge: pkce.challenge, 46 code_challenge_method: "S256", 47 }); 48 return `${INDIKO_URL}/auth/authorize?${params.toString()}`; 49} 50 51 52 53export interface TokenResponse { 54 access_token: string; 55 token_type: string; 56 expires_in: number; 57 refresh_token?: string; 58 me: string; 59 profile: { 60 name?: string; 61 email?: string; 62 photo?: string; 63 url?: string; 64 }; 65 scope: string; 66 iss: string; 67 role?: string; 68} 69 70export async function exchangeCodeForTokens( 71 code: string, 72 verifier: string 73): Promise<TokenResponse> { 74 const body: Record<string, string> = { 75 grant_type: "authorization_code", 76 code, 77 redirect_uri: REDIRECT_URI, 78 client_id: CLIENT_ID, 79 code_verifier: verifier, 80 }; 81 82 if (CLIENT_SECRET) { 83 body.client_secret = CLIENT_SECRET; 84 } 85 86 const response = await fetch(`${INDIKO_URL}/auth/token`, { 87 method: "POST", 88 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 89 body: new URLSearchParams(body), 90 }); 91 92 if (!response.ok) { 93 const text = await response.text(); 94 throw new Error(`Token exchange failed: ${response.status} ${text}`); 95 } 96 97 return response.json(); 98} 99 100export async function createSessionFromToken( 101 c: Context, 102 tokenResponse: TokenResponse 103): Promise<void> { 104 const payload: SessionPayload = { 105 sub: tokenResponse.me, 106 name: tokenResponse.profile.name || tokenResponse.me, 107 role: tokenResponse.role, 108 exp: Math.floor(Date.now() / 1000) + 86400 * 7, 109 }; 110 111 const token = await sign(payload, SESSION_SECRET); 112 113 setCookie(c, "session", token, { 114 httpOnly: true, 115 secure: !IS_DEV, 116 sameSite: "Lax", 117 path: "/", 118 maxAge: 86400 * 7, 119 }); 120} 121 122export async function getSession(c: Context): Promise<SessionPayload | null> { 123 const token = getCookie(c, "session"); 124 if (!token) return null; 125 126 try { 127 const payload = await verify(token, SESSION_SECRET); 128 if (typeof payload.exp === "number" && payload.exp < Date.now() / 1000) { 129 return null; 130 } 131 return payload as SessionPayload; 132 } catch { 133 return null; 134 } 135} 136 137export function clearSession(c: Context): void { 138 deleteCookie(c, "session", { path: "/" }); 139} 140 141export function checkRole(session: SessionPayload): boolean { 142 if (!REQUIRED_ROLE) return true; 143 return session.role === REQUIRED_ROLE; 144} 145 146export function storePKCE(c: Context, pkce: PKCEChallenge, state: string): void { 147 setCookie(c, "pkce_verifier", pkce.verifier, { 148 httpOnly: true, 149 secure: !IS_DEV, 150 sameSite: "Lax", 151 path: "/", 152 maxAge: 600, 153 }); 154 setCookie(c, "oauth_state", state, { 155 httpOnly: true, 156 secure: !IS_DEV, 157 sameSite: "Lax", 158 path: "/", 159 maxAge: 600, 160 }); 161} 162 163export function getPKCE(c: Context): { verifier: string | undefined; state: string | undefined } { 164 return { 165 verifier: getCookie(c, "pkce_verifier"), 166 state: getCookie(c, "oauth_state"), 167 }; 168} 169 170export function clearPKCE(c: Context): void { 171 deleteCookie(c, "pkce_verifier", { path: "/" }); 172 deleteCookie(c, "oauth_state", { path: "/" }); 173}