mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Every write to the per-sign-in Codex memory checked the account digest, but the reads did not. A credential reconnected to another ChatGPT account therefore tried the old account's working model first, sorted by its refusals and trusted its model list, until the first write rebound the entry, and the vision gate kept answering from the old account in the meantime. The client reads (working model, refusals, model list) now take the credential and treat an entry learned under another account as empty. The vision gate holds no account id, so the connect routes rebind the entry and the disconnect routes drop it, and the gate reads the entry as its process last bound it.
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { apiHandler, requireAdmin, HttpError } from "@/lib/api-handler";
|
|
import { prisma } from "@/lib/db";
|
|
import { auditLog } from "@/lib/auth/audit";
|
|
import { apiSuccess, getClientIp } from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { checkRateLimit } from "@/lib/rate-limit";
|
|
import {
|
|
codexCredentialKey,
|
|
forgetCodexSignIn,
|
|
} from "@/lib/ai/codex-slug-cache";
|
|
|
|
/**
|
|
* Operator-shared central Codex (ChatGPT subscription) — status + disconnect.
|
|
*
|
|
* GET /api/admin/central-codex — connection status + connected-at.
|
|
* DELETE /api/admin/central-codex — clear the shared credential.
|
|
*
|
|
* Cookie-only `requireAdmin()`: a Bearer token, even with `["*"]` scope, can
|
|
* never reach this. The encrypted token columns are NEVER returned — only the
|
|
* status string and the connected-at timestamp leave the server.
|
|
*/
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export const GET = apiHandler(async () => {
|
|
await requireAdmin();
|
|
annotate({ action: { name: "admin.central-codex.get" } });
|
|
|
|
const settings = await prisma.appSettings.findUnique({
|
|
where: { id: "singleton" },
|
|
select: {
|
|
adminCodexConnectionStatus: true,
|
|
adminCodexConnectedAt: true,
|
|
},
|
|
});
|
|
|
|
return apiSuccess({
|
|
status: settings?.adminCodexConnectionStatus ?? "disconnected",
|
|
connectedAt: settings?.adminCodexConnectedAt ?? null,
|
|
});
|
|
});
|
|
|
|
export const DELETE = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAdmin();
|
|
|
|
const rl = await checkRateLimit("admin-central-codex-disconnect", 5, 60_000);
|
|
if (!rl.allowed) throw new HttpError(429, "Too many requests");
|
|
|
|
await prisma.appSettings.upsert({
|
|
where: { id: "singleton" },
|
|
update: {
|
|
adminCodexAccessTokenEncrypted: null,
|
|
adminCodexRefreshTokenEncrypted: null,
|
|
adminCodexAccountIdEncrypted: null,
|
|
adminCodexTokenExpiresAt: null,
|
|
adminCodexConnectedAt: null,
|
|
adminCodexConnectionStatus: "disconnected",
|
|
},
|
|
create: { id: "singleton", adminCodexConnectionStatus: "disconnected" },
|
|
});
|
|
|
|
forgetCodexSignIn(codexCredentialKey.central);
|
|
|
|
await auditLog("admin.central-codex.disconnected", {
|
|
userId: user.id,
|
|
ipAddress: getClientIp(request),
|
|
});
|
|
annotate({ action: { name: "admin.central-codex.disconnect" } });
|
|
|
|
return apiSuccess({ disconnected: true });
|
|
});
|