import { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const"; import type { Express, Request, Response } from "express"; import * as db from "../db"; import { getSessionCookieOptions } from "./cookies"; import { sdk } from "./sdk"; import { ENV } from "./env"; function getQueryParam(req: Request, key: string): string | undefined { const value = req.query[key]; return typeof value === "string" ? value : undefined; } // Dev user for development mode const DEV_USER = { openId: "dev-user-001", name: "Dev User", email: "dev@localhost", loginMethod: "dev", }; // Check if HuggingFace OAuth is configured function isHfOAuthConfigured(): boolean { return !!(ENV.hfOAuthClientId && ENV.hfOAuthClientSecret); } // Get the base URL for redirects function getBaseUrl(req: Request): string { if (ENV.hfSpaceHost) { return `https://${ENV.hfSpaceHost}`; } const protocol = req.headers["x-forwarded-proto"] || req.protocol; const host = req.headers["x-forwarded-host"] || req.headers.host; return `${protocol}://${host}`; } export function registerOAuthRoutes(app: Express) { // HuggingFace OAuth login route app.get("/api/hf/login", (req: Request, res: Response) => { if (!isHfOAuthConfigured()) { res.status(400).json({ error: "HuggingFace OAuth not configured" }); return; } const baseUrl = getBaseUrl(req); const redirectUri = `${baseUrl}/api/hf/callback`; const scope = "openid profile"; const state = Buffer.from(JSON.stringify({ redirect: "/" })).toString("base64"); const authUrl = new URL("https://huggingface.co/oauth/authorize"); authUrl.searchParams.set("client_id", ENV.hfOAuthClientId); authUrl.searchParams.set("redirect_uri", redirectUri); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("scope", scope); authUrl.searchParams.set("state", state); console.log("[HF OAuth] Redirecting to:", authUrl.toString()); res.redirect(302, authUrl.toString()); }); // HuggingFace OAuth callback app.get("/api/hf/callback", async (req: Request, res: Response) => { const code = getQueryParam(req, "code"); const error = getQueryParam(req, "error"); if (error) { console.error("[HF OAuth] Error:", error); res.status(400).json({ error: `OAuth error: ${error}` }); return; } if (!code) { res.status(400).json({ error: "Missing authorization code" }); return; } try { const baseUrl = getBaseUrl(req); const redirectUri = `${baseUrl}/api/hf/callback`; // Exchange code for token const tokenResponse = await fetch("https://huggingface.co/oauth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: ENV.hfOAuthClientId, client_secret: ENV.hfOAuthClientSecret, code, redirect_uri: redirectUri, }), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error("[HF OAuth] Token exchange failed:", errorText); res.status(400).json({ error: "Token exchange failed" }); return; } const tokens = await tokenResponse.json() as { access_token: string }; // Get user info const userInfoResponse = await fetch("https://huggingface.co/oauth/userinfo", { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); if (!userInfoResponse.ok) { res.status(400).json({ error: "Failed to get user info" }); return; } const userInfo = await userInfoResponse.json() as { sub: string; name?: string; preferred_username?: string; email?: string; }; const openId = `hf-${userInfo.sub}`; const name = userInfo.name || userInfo.preferred_username || "HF User"; const email = userInfo.email || null; // Save user to database try { await db.upsertUser({ openId, name, email, loginMethod: "huggingface", lastSignedIn: new Date(), }); } catch (dbError) { console.log("[HF OAuth] Database not available, proceeding without persistence"); } // Create session const sessionToken = await sdk.createSessionToken(openId, { name, expiresInMs: ONE_YEAR_MS, }); const cookieOptions = getSessionCookieOptions(req); res.cookie(COOKIE_NAME, sessionToken, { ...cookieOptions, maxAge: ONE_YEAR_MS }); console.log("[HF OAuth] User logged in:", name); // Use client-side redirect instead of 302 - Safari has issues with cookies on redirects res.setHeader("Content-Type", "text/html"); res.send(` Redirecting...`); } catch (error) { console.error("[HF OAuth] Callback error:", error); res.status(500).json({ error: "OAuth callback failed" }); } }); // Development/Demo login route - enabled when HF OAuth is not configured app.get("/api/dev/login", async (req: Request, res: Response) => { // Allow demo login if HF OAuth is not configured (demo mode) const isDev = process.env.NODE_ENV !== "production"; const isDemo = !isHfOAuthConfigured(); // Allow demo login when no HF OAuth if (!isDev && !isDemo) { res.status(403).json({ error: "Login not available - please configure HuggingFace OAuth" }); return; } try { // Try to create or update dev user in database (optional) try { await db.upsertUser({ openId: DEV_USER.openId, name: DEV_USER.name, email: DEV_USER.email, loginMethod: DEV_USER.loginMethod, lastSignedIn: new Date(), }); } catch (dbError) { // Database not available - that's OK for dev mode console.log("[DevAuth] Database not available, proceeding without user persistence"); } // Create session token const sessionToken = await sdk.createSessionToken(DEV_USER.openId, { name: DEV_USER.name, expiresInMs: ONE_YEAR_MS, }); // Set session cookie const cookieOptions = getSessionCookieOptions(req); res.cookie(COOKIE_NAME, sessionToken, { ...cookieOptions, maxAge: ONE_YEAR_MS }); console.log("[DevAuth] Dev user logged in:", DEV_USER.email); // Use client-side redirect instead of 302 - Safari has issues with cookies on redirects res.setHeader("Content-Type", "text/html"); res.send(` Redirecting...`); } catch (error) { console.error("[DevAuth] Dev login failed:", error); res.status(500).json({ error: "Dev login failed" }); } }); // Endpoint to check auth config (used by frontend) app.get("/api/auth/config", (req: Request, res: Response) => { const hfOAuthEnabled = isHfOAuthConfigured(); res.json({ hfOAuthEnabled, // Dev/demo login is enabled in dev mode OR when HF OAuth is not configured devLoginEnabled: process.env.NODE_ENV !== "production" || !hfOAuthEnabled, }); }); app.get("/api/oauth/callback", async (req: Request, res: Response) => { const code = getQueryParam(req, "code"); const state = getQueryParam(req, "state"); if (!code || !state) { res.status(400).json({ error: "code and state are required" }); return; } try { const tokenResponse = await sdk.exchangeCodeForToken(code, state); const userInfo = await sdk.getUserInfo(tokenResponse.accessToken); if (!userInfo.openId) { res.status(400).json({ error: "openId missing from user info" }); return; } await db.upsertUser({ openId: userInfo.openId, name: userInfo.name || null, email: userInfo.email ?? null, loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null, lastSignedIn: new Date(), }); const sessionToken = await sdk.createSessionToken(userInfo.openId, { name: userInfo.name || "", expiresInMs: ONE_YEAR_MS, }); const cookieOptions = getSessionCookieOptions(req); res.cookie(COOKIE_NAME, sessionToken, { ...cookieOptions, maxAge: ONE_YEAR_MS }); res.redirect(302, "/"); } catch (error) { console.error("[OAuth] Callback failed", error); res.status(500).json({ error: "OAuth callback failed" }); } }); }