tfrere's picture
tfrere HF Staff
feat(storage): first-class data - no silent failures in the persistence pipeline
7a42df5
Raw
History Blame Contribute Delete
2.93 kB
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;
}