169c7d5006
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
89 lines
3.7 KiB
TypeScript
89 lines
3.7 KiB
TypeScript
/**
|
|
* Current-user routes: GET /api/me, PUT /api/me/password, GET|PUT /api/me/prefs.
|
|
* ui_prefs is free-form JSON (theme / lastProjectId / credentialGuideSeen, etc.): GET reads
|
|
* it whole, PUT shallow-merges (PATCH semantics) — several independent writers each write
|
|
* their own fields without clobbering each other.
|
|
*/
|
|
import { Hono } from "hono";
|
|
import type { MeResponse, PrefsResponse, UiPrefs } from "../../api/types.js";
|
|
import { toUserInfo } from "../../auth/service.js";
|
|
import type { AppEnv } from "../../auth/middleware.js";
|
|
import { readJson, requireString } from "../validate.js";
|
|
import type { AppDeps } from "../../app.js";
|
|
import { resolvePreviewTarget } from "../../services/preview-token.js";
|
|
|
|
export function meRoutes(deps: AppDeps): Hono<AppEnv> {
|
|
const app = new Hono<AppEnv>();
|
|
|
|
app.get("/", (c) => {
|
|
// previewIsolated depends on the host this request came in on, so it is computed
|
|
// here rather than stored: the same server answers on 127.0.0.1, localhost and
|
|
// possibly a LAN address, and only the first two have a loopback counterpart.
|
|
const target = resolvePreviewTarget(
|
|
c.req.url,
|
|
c.req.header("host"),
|
|
deps.config.previewOrigin,
|
|
deps.config,
|
|
);
|
|
return c.json({
|
|
user: toUserInfo(c.var.user),
|
|
previewIsolated: target !== null,
|
|
desktopMode: deps.desktop !== null,
|
|
sessionVia: c.var.sessionVia,
|
|
} satisfies MeResponse);
|
|
});
|
|
|
|
// Self-service password change (user settings): validates the old password; on success, the initial-password prompt disappears from GET /api/me.
|
|
// Desktop sessions may omit oldPassword: the seed password of a desktop-created root is
|
|
// random and never shown, so its holder has nothing to type — the shell's redeemed
|
|
// token already proved machine ownership.
|
|
app.put("/password", async (c) => {
|
|
const body = await readJson(c);
|
|
const newPassword = requireString(body, "newPassword", { label: "newPassword" });
|
|
const desktopSession = deps.desktop !== null && c.var.sessionVia === "desktop";
|
|
if (desktopSession && body.oldPassword === undefined) {
|
|
await deps.authService.setPasswordDesktop(c.var.user.userId, newPassword);
|
|
} else {
|
|
const oldPassword = requireString(body, "oldPassword", { label: "oldPassword" });
|
|
await deps.authService.changePassword(c.var.user.userId, oldPassword, newPassword);
|
|
}
|
|
return c.body(null, 204);
|
|
});
|
|
|
|
app.get("/prefs", (c) => {
|
|
const raw = deps.prefsRepo.get(c.var.user.userId);
|
|
let prefs: UiPrefs = {};
|
|
if (raw !== null) {
|
|
try {
|
|
prefs = JSON.parse(raw) as UiPrefs;
|
|
} catch {
|
|
prefs = {}; // Corrupted prefs fall back to an empty object
|
|
}
|
|
}
|
|
return c.json({ prefs } satisfies PrefsResponse);
|
|
});
|
|
|
|
// PATCH semantics: the request body is **shallow-merged** into existing prefs, not a
|
|
// full replace. prefs has several independent writers (lastProjectId /
|
|
// credentialGuideSeen, etc., each writing their own field); a full replace would wipe
|
|
// out each other's fields — e.g. writing lastProjectId when switching Projects would
|
|
// clear credentialGuideSeen, breaking the "show onboarding once ever" guarantee.
|
|
app.put("/prefs", async (c) => {
|
|
const body = await readJson(c);
|
|
const raw = deps.prefsRepo.get(c.var.user.userId);
|
|
let current: UiPrefs = {};
|
|
if (raw !== null) {
|
|
try {
|
|
current = JSON.parse(raw) as UiPrefs;
|
|
} catch {
|
|
current = {}; // Corrupted prefs fall back to an empty object (consistent with GET).
|
|
}
|
|
}
|
|
const merged = { ...current, ...(body as UiPrefs) };
|
|
deps.prefsRepo.set(c.var.user.userId, JSON.stringify(merged));
|
|
return c.json({ prefs: merged } satisfies PrefsResponse);
|
|
});
|
|
|
|
return app;
|
|
}
|