import { Router } from "express"; import type express from "express"; import { resolveUser, extractToken, handleOAuthAuthorize, handleOAuthCallback, handleOAuthLogout, } from "../auth.js"; import { setUserToken, ensureDatasetExists } from "../hf-storage.js"; import { ensurePublishedRestored } from "../persistence.js"; export interface AuthContext { oauthEnabled: boolean; } /** * Middleware that requires editor-level access when OAuth is enabled. * Reusable across route modules. */ export function createRequireEditor(ctx: AuthContext): express.RequestHandler { return async (req, res, next) => { if (!ctx.oauthEnabled) return next(); const token = extractToken(req.headers.cookie); const user = await resolveUser(token); if (!user || !user.canEdit) { res.status(403).json({ error: "Unauthorized" }); return; } next(); }; } export function createAuthRouter(ctx: AuthContext): Router { const router = Router(); if (ctx.oauthEnabled) { router.get("/oauth/authorize", handleOAuthAuthorize); router.get("/auth/callback", handleOAuthCallback); // POST is the primary method; GET fallback is handy for ops // (link from a status page, curl debugging). Both clear the // httpOnly cookie - the client can't do that itself. router.post("/api/auth/logout", handleOAuthLogout); router.get("/api/auth/logout", handleOAuthLogout); } router.get("/api/auth/status", async (req, res) => { if (!ctx.oauthEnabled) { res.json({ authenticated: true, canEdit: true, user: null }); return; } const token = extractToken(req.headers.cookie); const user = await resolveUser(token); if (!user) { res.json({ authenticated: false, canEdit: false, user: null, loginUrl: "/oauth/authorize" }); return; } if (user.canEdit && token) { setUserToken(token); ensurePublishedRestored(token).catch(() => {}); // Eagerly try to create the backing dataset on first login // (instead of waiting for the first edit + 12s debounce). If // creation fails - missing manage-repos scope, org policy // blocking the create, etc. - the error lands in the // storage-status tracker right away, and the editor's // SyncIndicator can flash a red "Cloud storage error" on the // first poll. Without this, a misconfigured fork looks // perfectly healthy until the user has typed something and // waited half a minute, by which point they've already // assumed everything is fine. ensureDatasetExists(token).catch((err) => { console.warn("[auth] eager ensureDatasetExists failed:", (err as Error).message); }); } res.json({ authenticated: true, canEdit: user.canEdit, user: { name: user.name, fullName: user.fullName, avatarUrl: user.avatarUrl, }, }); }); return router; }