082ab08e0b
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
/**
|
|
* Desktop-mode routes: POST /api/desktop/shutdown, plus the shared desktop-mode guard
|
|
* that turns off multi-user surfaces (see rejectInDesktopMode).
|
|
*
|
|
* The shutdown route is authenticated by the shell's Bearer token, not the cookie
|
|
* session (the shell holds no cookie), so it mounts OUTSIDE authMiddleware and only
|
|
* when desktop mode is enabled. Responds 202 first, then triggers the graceful
|
|
* shutdown a beat later so the response isn't cut off by the closing listener.
|
|
*/
|
|
import { Hono } from "hono";
|
|
import type { MiddlewareHandler } from "hono";
|
|
import { HttpError } from "../errors.js";
|
|
import type { AppDeps } from "../../app.js";
|
|
|
|
/**
|
|
* Guard for user-management surfaces (admin users, Project members): the desktop app is
|
|
* single-user, so the whole surface answers 403 with a dedicated code rather than being
|
|
* unmounted — a stray client gets a clear, localizable error instead of a 404. Existing
|
|
* users and memberships in the data root are untouched; only the management routes are
|
|
* closed while the server runs under the desktop shell.
|
|
*/
|
|
export function rejectInDesktopMode(deps: AppDeps): MiddlewareHandler {
|
|
return async (_c, next) => {
|
|
if (deps.desktop !== null) {
|
|
throw new HttpError(
|
|
403,
|
|
"desktop_single_user",
|
|
"User management is disabled in the desktop app (single-user mode).",
|
|
);
|
|
}
|
|
await next();
|
|
};
|
|
}
|
|
|
|
/** Delay between answering 202 and starting shutdown: lets the response flush. */
|
|
const SHUTDOWN_DELAY_MS = 50;
|
|
|
|
export function desktopRoutes(deps: AppDeps): Hono {
|
|
const app = new Hono();
|
|
|
|
app.post("/shutdown", (c) => {
|
|
const desktop = deps.desktop;
|
|
if (!desktop) throw new HttpError(404, "not_found", "Desktop mode is not enabled.");
|
|
const header = c.req.header("authorization") ?? "";
|
|
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
if (token === "" || !desktop.verifyToken(token)) {
|
|
throw new HttpError(401, "unauthorized", "Invalid desktop token.");
|
|
}
|
|
setTimeout(() => desktop.requestShutdown(), SHUTDOWN_DELAY_MS).unref();
|
|
return c.body(null, 202);
|
|
});
|
|
|
|
return app;
|
|
}
|