mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Bucket C of the v1.4.3 polish pass. Three independent bugs in the KI-provider settings, fixed together because they all live on the same panel: 1. **Selecting "OpenAI" left no way to provide a key.** The schema stored Anthropic and Local API keys, but OPENAI was a valid `aiProvider` value with no matching credential column — the resolver silently fell through to the admin-level OpenAI key. So a self-hosted user who picked OpenAI and typed nothing got the admin's key (might not exist), and a user who *wanted* their own OpenAI key had no UI surface to enter it. Migration `0026_user_ai_openai_key` adds `users.ai_openai_key_ encrypted`. The resolver's `OPENAI` branch now picks up the user-level key first and only falls back to admin if the user slot is empty. The settings UI grew an OpenAI-key field gated on `provider === "OPENAI"` mirroring the Anthropic flow. The `/api/ai/test` override schema accepts `openaiKey` so the "Verbindung testen" button can validate an unsaved key without persisting plaintext. 2. **Model field was a freetext Input, easy to typo.** Replaced with a `<select>` populated from `MODEL_PRESETS` per provider: gpt-4o / gpt-4o-mini / gpt-4-turbo / gpt-5 / o3-mini for OPENAI; claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5 / claude- 3-5-sonnet-latest for ANTHROPIC; llama3.1:8b / llama3.1:70b / mistral / qwen2.5 for LOCAL. An "Eigenes…" sentinel still surfaces the freetext Input so power users can target preview models or fine-tunes. The dropdown disables when no provider is chosen — saves the user from filling a model that won't be used. 3. **"Mit ChatGPT verbinden" silently bounced to chatgpt.com login with no OAuth grant.** The authorize URL was built without a `client_id` parameter, so chatgpt.com couldn't recognise the request as an OAuth handshake — it just rendered the regular login form. The user came back, nothing connected, the button "did nothing". Added `CODEX_OAUTH_CLIENT_ID` env var. The URL builder now throws `CodexOAuthNotConfiguredError` if it's unset; the authorize route maps that to a 503 with an actionable message instead of dispatching the dead URL. The settings card reads `codexOauthConfigured` from `/api/insights/settings` and hides the Connect button when the flag is false, replacing it with a tiny italicised "ChatGPT-OAuth ist auf dieser Instanz nicht konfiguriert — nutze stattdessen einen eigenen API-Key unten." `CHATGPT_OAUTH` was also removed as a manually-selectable provider option in the dropdown — the top connect button is the path; having both confused the affordance. Refresh + token-exchange both forward the client_id now too. Test fixture seeds `process.env.CODEX_OAUTH_CLIENT_ID="test_client_id"` in `beforeEach` so the existing URL-shape assertion still works, plus a new test asserts the unset case throws. Tests + typecheck green (670/670 — one new assertion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
|
|
import { checkRateLimit } from "@/lib/rate-limit";
|
|
import {
|
|
generatePKCE,
|
|
generateState,
|
|
buildAuthorizationUrl,
|
|
CodexOAuthNotConfiguredError,
|
|
isCodexOAuthConfigured,
|
|
} from "@/lib/ai/codex-oauth";
|
|
import { cookies } from "next/headers";
|
|
import { annotate } from "@/lib/logging/context";
|
|
|
|
export const GET = apiHandler(async (_request: NextRequest) => {
|
|
const { user } = await requireAuth();
|
|
|
|
// Surface configuration errors as 503 instead of silently redirecting
|
|
// to a dead chatgpt.com URL. The v1.4.2 build dropped the `client_id`
|
|
// out of the authorize URL — the user landed on the chatgpt.com login
|
|
// page, no callback ever fired, and the connect button "did nothing".
|
|
if (!isCodexOAuthConfigured()) {
|
|
throw new HttpError(
|
|
503,
|
|
"Codex OAuth is not configured on this instance — set CODEX_OAUTH_CLIENT_ID",
|
|
);
|
|
}
|
|
|
|
const rl = await checkRateLimit(`codex-authorize:${user.id}`, 5, 60_000);
|
|
if (!rl.allowed) throw new HttpError(429, "Too many requests");
|
|
|
|
const { verifier, challenge } = generatePKCE();
|
|
const state = generateState();
|
|
|
|
const appUrl = process.env.APP_URL || process.env.NEXT_PUBLIC_APP_URL;
|
|
if (!appUrl) throw new Error("APP_URL not configured");
|
|
|
|
const redirectUri = `${appUrl}/api/auth/codex/callback`;
|
|
|
|
const cookieStore = await cookies();
|
|
const cookieOptions = {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "lax" as const,
|
|
path: "/",
|
|
maxAge: 300,
|
|
};
|
|
|
|
cookieStore.set("codex_verifier", verifier, cookieOptions);
|
|
cookieStore.set("codex_state", state, cookieOptions);
|
|
|
|
let authUrl: string;
|
|
try {
|
|
authUrl = buildAuthorizationUrl({
|
|
codeChallenge: challenge,
|
|
state,
|
|
redirectUri,
|
|
});
|
|
} catch (err) {
|
|
// Race between the env check above and the URL build — extremely
|
|
// unlikely but the typed error path is cheap insurance.
|
|
if (err instanceof CodexOAuthNotConfiguredError) {
|
|
throw new HttpError(503, err.message);
|
|
}
|
|
throw err;
|
|
}
|
|
annotate({ action: { name: "codex.oauth.authorize" } });
|
|
|
|
return NextResponse.redirect(authUrl);
|
|
});
|