Spaces:
Sleeping
Sleeping
| 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(`<!DOCTYPE html> | |
| <html><head> | |
| <meta http-equiv="refresh" content="0;url=/"> | |
| <script>window.location.href="/";</script> | |
| </head><body>Redirecting...</body></html>`); | |
| } 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(`<!DOCTYPE html> | |
| <html><head> | |
| <meta http-equiv="refresh" content="0;url=/"> | |
| <script>window.location.href="/";</script> | |
| </head><body>Redirecting...</body></html>`); | |
| } 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" }); | |
| } | |
| }); | |
| } | |