fix(dashboard): exempt SPA shell from bearer-token auth
The bearer-token middleware was mounted globally on the Express app, so the initial GET /?token=<token> request for index.html was rejected with 401 before the frontend JS could even load and capture the token from the URL. The server-side query-string fallback only accepts fn_token=, while the banner URL uses ?token= (which only the frontend captures), so the bootstrap request had no valid auth path. Fix: gate auth on /api/* only. The SPA shell (index.html, /assets/*, favicon) is public — it contains no secrets, and the frontend auth.ts module captures the token into localStorage and installs a fetch wrapper that injects Authorization: Bearer on every /api/* call. Also: add .env / .env.local to .gitignore so local FUSION_DAEMON_TOKEN values don't accidentally get committed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,17 @@ export const TOKEN_QUERY_PARAM = "fn_token";
|
||||
/** Paths that are exempt from authentication (liveness probes). */
|
||||
const EXEMPT_PATHS = ["/api/health"];
|
||||
|
||||
/**
|
||||
* Only /api/* paths are gated by this middleware. The SPA shell (index.html,
|
||||
* /assets/*, favicon, etc.) must load unauthenticated so the frontend JS can
|
||||
* run, read ?token= off the URL, and start injecting Bearer headers on API
|
||||
* calls. Without this exemption the browser gets 401 on the very first GET /
|
||||
* and never gets a chance to capture the token.
|
||||
*/
|
||||
function isApiPath(path: string): boolean {
|
||||
return path === "/api" || path.startsWith("/api/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if daemon auth should be active.
|
||||
* Auth is enabled when FUSION_DAEMON_TOKEN env var is set OR daemon options are provided.
|
||||
@@ -133,7 +144,13 @@ export function createAuthMiddleware(token: string) {
|
||||
};
|
||||
|
||||
return function authMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
// Always allow exempt paths
|
||||
// The SPA shell and static assets are public — only /api/* is gated.
|
||||
if (!isApiPath(req.path)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Always allow exempt paths (liveness probes)
|
||||
if (isExemptPath(req.path)) {
|
||||
next();
|
||||
return;
|
||||
|
||||
@@ -392,8 +392,12 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
}));
|
||||
|
||||
// Daemon mode: bearer token authentication middleware
|
||||
// Auth is enabled when daemon option is provided OR FUSION_DAEMON_TOKEN env var is set
|
||||
// The middleware itself exempts /api/health for liveness probes
|
||||
// Auth is enabled when daemon option is provided OR FUSION_DAEMON_TOKEN env var is set.
|
||||
// The middleware exempts /api/health and everything outside /api/ — the SPA shell
|
||||
// (index.html + built assets) is public so the browser can load the frontend JS
|
||||
// that then captures ?token= from the URL and injects a Bearer header on every
|
||||
// /api/* call. WebSocket upgrades are gated separately in setupTerminalWebSocket /
|
||||
// setupBadgeWebSocket.
|
||||
const daemonToken = options?.daemon?.token ?? process.env.FUSION_DAEMON_TOKEN;
|
||||
if (daemonToken) {
|
||||
app.use(createAuthMiddleware(daemonToken));
|
||||
|
||||
Reference in New Issue
Block a user