feat(desktop,server,web): app icons, completion notifications, single-user mode, bundled penguin CLI (#226)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Admin user-backend routes: only the built-in admin can use these (403 for non-admins).
|
||||
* Admin user-backend routes: only the built-in admin can use these (403 for non-admins),
|
||||
* and desktop mode rejects the whole surface (single-user; 403 `desktop_single_user`).
|
||||
* GET|POST /api/admin/users, POST /api/admin/users/:userId/password, DELETE /api/admin/users/:userId.
|
||||
*/
|
||||
import { Hono } from "hono";
|
||||
import type { AdminUserCreateResponse, AdminUsersResponse } from "../../api/types.js";
|
||||
import { HttpError } from "../errors.js";
|
||||
import { rejectInDesktopMode } from "./desktop.js";
|
||||
import type { AppEnv } from "../../auth/middleware.js";
|
||||
import { pathParam, readJson, requireString } from "../validate.js";
|
||||
import type { AppDeps } from "../../app.js";
|
||||
@@ -12,6 +14,7 @@ import type { AppDeps } from "../../app.js";
|
||||
export function adminUsersRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const app = new Hono<AppEnv>();
|
||||
|
||||
app.use("*", rejectInDesktopMode(deps));
|
||||
app.use("*", async (c, next) => {
|
||||
if (!c.var.user.isAdmin) {
|
||||
throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
/**
|
||||
* Desktop-mode routes: POST /api/desktop/shutdown.
|
||||
* Desktop-mode routes: POST /api/desktop/shutdown, plus the shared desktop-mode guard
|
||||
* that turns off multi-user surfaces (see rejectInDesktopMode).
|
||||
*
|
||||
* Authenticated by the shell's Bearer token, not the cookie session (the shell holds no
|
||||
* cookie), so this 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.
|
||||
* 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;
|
||||
|
||||
|
||||
@@ -2,16 +2,20 @@
|
||||
* Member authorization routes:
|
||||
* GET|POST /api/projects/:p/members, DELETE /api/projects/:p/members/:userId.
|
||||
* Reading requires access; adding/removing is owner-only (validated inside the service).
|
||||
* Desktop mode rejects the whole surface (single-user; 403 `desktop_single_user`).
|
||||
*/
|
||||
import { Hono } from "hono";
|
||||
import type { MemberAddResponse, MembersResponse } from "../../api/types.js";
|
||||
import type { AppEnv } from "../../auth/middleware.js";
|
||||
import { rejectInDesktopMode } from "./desktop.js";
|
||||
import { pathParam, readJson, requireString, requireValidId } from "../validate.js";
|
||||
import type { AppDeps } from "../../app.js";
|
||||
|
||||
export function membersRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const app = new Hono<AppEnv>();
|
||||
|
||||
app.use("*", rejectInDesktopMode(deps));
|
||||
|
||||
app.get("/", (c) => {
|
||||
// Defensive id validation (FD-4).
|
||||
const members = deps.projectService.listMembers(
|
||||
|
||||
Reference in New Issue
Block a user