Files
HealthLog/src/app/api/admin/central-codex/device-poll/route.ts
T
Marc-André Bombeck 86a3856ae9 fix(i18n): drop spaced dashes as separators from every visible string
A dash between two parts of a sentence ("{what} — tomorrow", "Could not
record {name} — try again", "End {name} — …") stands in for a comma, a
colon or a full stop the sentence should say. Every such string in the
seven bundles now says it in plain words. A range keeps its unspaced en
dash ("70–100 mmHg", "{start}–{end}"); that is how a range is written,
not a pause, and the spaced range forms became unspaced ones.

Labels the components put together themselves follow the same rule:
aria-labels read "Move up: Ramipril", a medication and its dose read
"Ramipril 5 mg", page titles use "|", Coach prefills are two sentences,
and the API's user-facing error messages lost their dashes too.

The bundle guard now refuses " — " and " – " beside " · ", with an empty
allowlist, and a sibling guard walks src/components and src/app for a
string or template literal that joins its parts with a spaced dash.
2026-10-09 16:05:32 +02:00

107 lines
3.5 KiB
TypeScript

import { NextRequest } from "next/server";
import { apiHandler, requireAdmin, HttpError } from "@/lib/api-handler";
import { checkRateLimit } from "@/lib/rate-limit";
import { pollDeviceCode, encryptAdminCodexCreds } from "@/lib/ai/codex-oauth";
import { cookies } from "next/headers";
import { decrypt } from "@/lib/crypto";
import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { getClientIp, apiSuccess, apiError } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
/**
* Operator-shared central Codex — device-code poll (admin only).
*
* Cookie-only `requireAdmin()`. The client polls this every `intervalSeconds`;
* on the operator finishing the approval on chatgpt.com we exchange the code,
* then persist the three token pieces (access + refresh + `ChatGPT-Account-ID`),
* each AES-256-GCM encrypted, on the `AppSettings` singleton. The device cookie
* is deleted on success. Tokens are never logged and never leave the server.
*/
export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAdmin();
const rl = await checkRateLimit(
`central-codex-device-poll:${user.id}`,
60,
60_000,
);
if (!rl.allowed) throw new HttpError(429, "Too many requests");
const cookieStore = await cookies();
const cookie = cookieStore.get("central_codex_device")?.value;
if (!cookie) {
return apiError("No device-auth attempt in progress", 400);
}
let deviceAuthId: string;
let userCode: string;
try {
const decoded = JSON.parse(decrypt(cookie)) as {
deviceAuthId: string;
userCode: string;
};
deviceAuthId = decoded.deviceAuthId;
userCode = decoded.userCode;
} catch {
cookieStore.delete("central_codex_device");
return apiError("Invalid device-auth state. Restart the flow", 400);
}
let result;
try {
result = await pollDeviceCode({ deviceAuthId, userCode });
} catch (err) {
const message = err instanceof Error ? err.message : "unknown";
annotate({ meta: { central_codex_device_poll_error: message } });
return apiError(
"Central Codex device-poll failed. Please restart the flow",
502,
);
}
if (result.status === "pending") {
annotate({
action: { name: "admin.central-codex.device.poll" },
meta: { pending: true },
});
return apiSuccess({ status: "pending" });
}
const enc = encryptAdminCodexCreds(result.creds);
await prisma.appSettings.upsert({
where: { id: "singleton" },
update: {
adminCodexAccessTokenEncrypted: enc.accessEncrypted,
adminCodexRefreshTokenEncrypted: enc.refreshEncrypted,
adminCodexAccountIdEncrypted: enc.accountIdEncrypted,
adminCodexTokenExpiresAt: enc.expiresAt,
adminCodexConnectedAt: new Date(),
adminCodexConnectionStatus: "connected",
},
create: {
id: "singleton",
adminCodexAccessTokenEncrypted: enc.accessEncrypted,
adminCodexRefreshTokenEncrypted: enc.refreshEncrypted,
adminCodexAccountIdEncrypted: enc.accountIdEncrypted,
adminCodexTokenExpiresAt: enc.expiresAt,
adminCodexConnectedAt: new Date(),
adminCodexConnectionStatus: "connected",
},
});
cookieStore.delete("central_codex_device");
await auditLog("admin.central-codex.connected", {
userId: user.id,
ipAddress: getClientIp(request),
details: { method: "device_code" },
});
annotate({
action: { name: "admin.central-codex.device.poll" },
meta: { connected: true },
});
return apiSuccess({ status: "connected" });
});