mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
feat(oauth): add Codex OAuth authorize/callback/disconnect routes and admin AI settings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
401e78e01c
commit
fde3a0941a
5 files changed
+266
No files matched your search
@@ -0,0 +1,103 @@
|
||||
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, safeJson } from "@/lib/api-response";
|
||||
import { encrypt } from "@/lib/crypto";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
await requireAdmin();
|
||||
annotate({ action: { name: "admin.ai-settings.get" } });
|
||||
|
||||
const settings = await prisma.appSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
});
|
||||
|
||||
const hasKey = Boolean(settings?.adminAiKeyEncrypted);
|
||||
const model = settings?.adminAiModel ?? "gpt-4o-mini";
|
||||
const baseUrl = settings?.adminAiBaseUrl ?? "https://api.openai.com/v1";
|
||||
|
||||
return apiSuccess({
|
||||
hasKey,
|
||||
keyPreview: hasKey && settings?.adminAiKeyEncrypted
|
||||
? `${settings.adminAiKeyEncrypted.slice(0, 7)}...`
|
||||
: null,
|
||||
model,
|
||||
baseUrl,
|
||||
});
|
||||
});
|
||||
|
||||
export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAdmin();
|
||||
annotate({ action: { name: "admin.ai-settings.update" } });
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const { apiKey, model, baseUrl } = body as {
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
};
|
||||
|
||||
const updates: Record<string, unknown> = {};
|
||||
const auditDetails: Record<string, unknown> = {};
|
||||
|
||||
if (apiKey !== undefined) {
|
||||
const trimmed = apiKey.trim();
|
||||
if (trimmed) {
|
||||
updates.adminAiKeyEncrypted = encrypt(trimmed);
|
||||
auditDetails.apiKeyUpdated = true;
|
||||
} else {
|
||||
updates.adminAiKeyEncrypted = null;
|
||||
auditDetails.apiKeyUpdated = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (model !== undefined) {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) throw new HttpError(422, "Model cannot be empty");
|
||||
updates.adminAiModel = trimmed;
|
||||
auditDetails.model = trimmed;
|
||||
}
|
||||
|
||||
if (baseUrl !== undefined) {
|
||||
const trimmed = baseUrl.trim();
|
||||
if (!trimmed) throw new HttpError(422, "Base URL cannot be empty");
|
||||
if (!trimmed.startsWith("https://")) {
|
||||
throw new HttpError(422, "Base URL must start with https://");
|
||||
}
|
||||
updates.adminAiBaseUrl = trimmed;
|
||||
auditDetails.baseUrl = trimmed;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
throw new HttpError(422, "No valid fields");
|
||||
}
|
||||
|
||||
const settings = await prisma.appSettings.upsert({
|
||||
where: { id: "singleton" },
|
||||
update: updates,
|
||||
create: { id: "singleton", ...updates },
|
||||
});
|
||||
|
||||
await auditLog("admin.ai-settings.update", {
|
||||
userId: user.id,
|
||||
ipAddress: getClientIp(request),
|
||||
details: auditDetails,
|
||||
});
|
||||
|
||||
const hasKey = Boolean(settings.adminAiKeyEncrypted);
|
||||
|
||||
return apiSuccess({
|
||||
hasKey,
|
||||
keyPreview: hasKey && settings.adminAiKeyEncrypted
|
||||
? `${settings.adminAiKeyEncrypted.slice(0, 7)}...`
|
||||
: null,
|
||||
model: settings.adminAiModel,
|
||||
baseUrl: settings.adminAiBaseUrl,
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user