mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
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.
107 lines
3.5 KiB
TypeScript
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" });
|
|
});
|