feat: clean routing - /editor for auth, / for published article
Browse files- GET / serves published HTML if available, otherwise login page
- GET /editor requires HF OAuth, serves the SPA editor
- In dev mode (no SPACE_ID), / serves the editor directly
- Cleaner login page with styled "Sign in with HF" button
Made-with: Cursor
- backend/src/server.ts +66 -31
backend/src/server.ts
CHANGED
|
@@ -260,50 +260,85 @@ const staticDir =
|
|
| 260 |
? join(__dirname, "..", "frontend-dist")
|
| 261 |
: join(__dirname, "..", "..", "frontend", "dist");
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
if (existsSync(staticDir)) {
|
| 264 |
app.use(express.static(staticDir));
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 268 |
if (oauthEnabled) {
|
| 269 |
const token = extractToken(req.headers.cookie);
|
| 270 |
const user = await resolveUser(token);
|
| 271 |
|
| 272 |
if (!user || !user.canEdit) {
|
| 273 |
-
//
|
| 274 |
-
const
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
const publishedPath = join(DATA_DIR, "published", safeName, "index.html");
|
| 278 |
-
|
| 279 |
-
if (existsSync(publishedPath)) {
|
| 280 |
-
res.sendFile(publishedPath);
|
| 281 |
-
return;
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
// No published version: show a simple message
|
| 285 |
-
if (!user) {
|
| 286 |
-
res.status(200).send(`
|
| 287 |
-
<!DOCTYPE html>
|
| 288 |
-
<html>
|
| 289 |
-
<head><title>Collab Editor</title></head>
|
| 290 |
-
<body style="display:flex;align-items:center;justify-content:center;height:100vh;margin:0;font-family:system-ui;background:#0a0a0a;color:#aaa;">
|
| 291 |
-
<div style="text-align:center;">
|
| 292 |
-
<h1 style="color:#fff;">This article is not yet published</h1>
|
| 293 |
-
<p>Log in with your Hugging Face account to edit.</p>
|
| 294 |
-
<a href="/oauth/authorize" style="color:#958DF1;">Log in</a>
|
| 295 |
-
</div>
|
| 296 |
-
</body>
|
| 297 |
-
</html>
|
| 298 |
-
`);
|
| 299 |
-
return;
|
| 300 |
-
}
|
| 301 |
}
|
| 302 |
}
|
| 303 |
|
| 304 |
-
// Authenticated editors get the SPA
|
| 305 |
res.sendFile(join(staticDir, "index.html"));
|
| 306 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
}
|
| 308 |
|
| 309 |
// ---------- Start ----------
|
|
|
|
| 260 |
? join(__dirname, "..", "frontend-dist")
|
| 261 |
: join(__dirname, "..", "..", "frontend", "dist");
|
| 262 |
|
| 263 |
+
function getDocName(req: express.Request): string {
|
| 264 |
+
const raw = (req.query.doc as string) || "default";
|
| 265 |
+
return raw.replace(/[^a-zA-Z0-9_-]/g, "_");
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function getPublishedPath(docName: string): string {
|
| 269 |
+
return join(DATA_DIR, "published", docName, "index.html");
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
function sendLoginPage(res: express.Response) {
|
| 273 |
+
res.status(200).send(`<!DOCTYPE html>
|
| 274 |
+
<html lang="en">
|
| 275 |
+
<head>
|
| 276 |
+
<meta charset="utf-8" />
|
| 277 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 278 |
+
<title>Collab Editor</title>
|
| 279 |
+
<style>
|
| 280 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 281 |
+
body { display: flex; align-items: center; justify-content: center; height: 100vh;
|
| 282 |
+
font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #aaa; }
|
| 283 |
+
.card { text-align: center; max-width: 420px; padding: 2rem; }
|
| 284 |
+
h1 { color: #fff; font-size: 1.5rem; margin-bottom: 0.5rem; }
|
| 285 |
+
p { margin-bottom: 1.5rem; line-height: 1.5; }
|
| 286 |
+
a.btn { display: inline-block; padding: 0.6rem 1.5rem; border-radius: 8px;
|
| 287 |
+
background: #958DF1; color: #fff; text-decoration: none; font-weight: 500;
|
| 288 |
+
transition: opacity 0.15s; }
|
| 289 |
+
a.btn:hover { opacity: 0.85; }
|
| 290 |
+
</style>
|
| 291 |
+
</head>
|
| 292 |
+
<body>
|
| 293 |
+
<div class="card">
|
| 294 |
+
<h1>This article is not yet published</h1>
|
| 295 |
+
<p>Log in with your Hugging Face account to access the editor.</p>
|
| 296 |
+
<a class="btn" href="/oauth/authorize">Sign in with Hugging Face</a>
|
| 297 |
+
</div>
|
| 298 |
+
</body>
|
| 299 |
+
</html>`);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
if (existsSync(staticDir)) {
|
| 303 |
app.use(express.static(staticDir));
|
| 304 |
|
| 305 |
+
// GET /editor → requires auth, serves the SPA
|
| 306 |
+
app.get("/editor", async (req, res) => {
|
| 307 |
if (oauthEnabled) {
|
| 308 |
const token = extractToken(req.headers.cookie);
|
| 309 |
const user = await resolveUser(token);
|
| 310 |
|
| 311 |
if (!user || !user.canEdit) {
|
| 312 |
+
// Not logged in or no write access → redirect to OAuth login
|
| 313 |
+
const docParam = req.query.doc ? `?doc=${req.query.doc}` : "";
|
| 314 |
+
res.redirect(`/oauth/authorize`);
|
| 315 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
}
|
| 317 |
}
|
| 318 |
|
|
|
|
| 319 |
res.sendFile(join(staticDir, "index.html"));
|
| 320 |
});
|
| 321 |
+
|
| 322 |
+
// GET / (and all other paths) → public article or login prompt
|
| 323 |
+
app.get("*", async (req, res) => {
|
| 324 |
+
if (!oauthEnabled) {
|
| 325 |
+
// Dev mode: serve editor directly
|
| 326 |
+
res.sendFile(join(staticDir, "index.html"));
|
| 327 |
+
return;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// In production with OAuth: serve the published article if it exists
|
| 331 |
+
const docName = getDocName(req);
|
| 332 |
+
const publishedPath = getPublishedPath(docName);
|
| 333 |
+
|
| 334 |
+
if (existsSync(publishedPath)) {
|
| 335 |
+
res.sendFile(publishedPath);
|
| 336 |
+
return;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
// No published version → login page
|
| 340 |
+
sendLoginPage(res);
|
| 341 |
+
});
|
| 342 |
}
|
| 343 |
|
| 344 |
// ---------- Start ----------
|