diff --git a/src/app/api/admin/ai-settings/route.ts b/src/app/api/admin/ai-settings/route.ts index 27fc07a07..0d988d575 100644 --- a/src/app/api/admin/ai-settings/route.ts +++ b/src/app/api/admin/ai-settings/route.ts @@ -3,7 +3,8 @@ 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 { encrypt, decrypt } from "@/lib/crypto"; +import { checkRateLimit } from "@/lib/rate-limit"; import { annotate } from "@/lib/logging/context"; export const dynamic = "force-dynamic"; @@ -22,8 +23,8 @@ export const GET = apiHandler(async () => { return apiSuccess({ hasKey, - keyPreview: hasKey && settings?.adminAiKeyEncrypted - ? `${settings.adminAiKeyEncrypted.slice(0, 7)}...` + keyPreview: settings?.adminAiKeyEncrypted + ? `...${decrypt(settings.adminAiKeyEncrypted).slice(-4)}` : null, model, baseUrl, @@ -34,6 +35,9 @@ export const PUT = apiHandler(async (request: NextRequest) => { const { user } = await requireAdmin(); annotate({ action: { name: "admin.ai-settings.update" } }); + const rl = await checkRateLimit("admin-ai-settings", 10, 60_000); + if (!rl.allowed) throw new HttpError(429, "Too many requests"); + const { data: body, error: jsonError } = await safeJson(request); if (jsonError) return jsonError; @@ -94,8 +98,8 @@ export const PUT = apiHandler(async (request: NextRequest) => { return apiSuccess({ hasKey, - keyPreview: hasKey && settings.adminAiKeyEncrypted - ? `${settings.adminAiKeyEncrypted.slice(0, 7)}...` + keyPreview: settings.adminAiKeyEncrypted + ? `...${decrypt(settings.adminAiKeyEncrypted).slice(-4)}` : null, model: settings.adminAiModel, baseUrl: settings.adminAiBaseUrl, diff --git a/src/app/api/auth/codex/authorize/route.ts b/src/app/api/auth/codex/authorize/route.ts index b38451206..0f186b3da 100644 --- a/src/app/api/auth/codex/authorize/route.ts +++ b/src/app/api/auth/codex/authorize/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { apiHandler, requireAuth } from "@/lib/api-handler"; +import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler"; +import { checkRateLimit } from "@/lib/rate-limit"; import { generatePKCE, generateState, @@ -9,7 +10,10 @@ import { cookies } from "next/headers"; import { annotate } from "@/lib/logging/context"; export const GET = apiHandler(async (_request: NextRequest) => { - await requireAuth(); + const { user } = await requireAuth(); + + 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(); diff --git a/src/app/api/auth/codex/callback/route.ts b/src/app/api/auth/codex/callback/route.ts index 29c5b72b7..c4c58bacb 100644 --- a/src/app/api/auth/codex/callback/route.ts +++ b/src/app/api/auth/codex/callback/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { apiHandler, requireAuth } from "@/lib/api-handler"; +import { checkRateLimit } from "@/lib/rate-limit"; import { exchangeCodeForTokens, encryptTokens } from "@/lib/ai/codex-oauth"; import { prisma } from "@/lib/db"; import { auditLog } from "@/lib/auth/audit"; @@ -9,6 +10,13 @@ import { annotate } from "@/lib/logging/context"; export const GET = apiHandler(async (request: NextRequest) => { const { user } = await requireAuth(); + + const rl = await checkRateLimit(`codex-callback:${user.id}`, 10, 60_000); + if (!rl.allowed) { + const appUrl = process.env.APP_URL || process.env.NEXT_PUBLIC_APP_URL; + return NextResponse.redirect(`${appUrl}/settings?codex_error=rate_limited`); + } + const url = new URL(request.url); const code = url.searchParams.get("code"); diff --git a/src/app/api/auth/codex/disconnect/route.ts b/src/app/api/auth/codex/disconnect/route.ts index 5ea6afa27..6b62cd7c0 100644 --- a/src/app/api/auth/codex/disconnect/route.ts +++ b/src/app/api/auth/codex/disconnect/route.ts @@ -1,5 +1,6 @@ import { NextRequest } from "next/server"; -import { apiHandler, requireAuth } from "@/lib/api-handler"; +import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler"; +import { checkRateLimit } from "@/lib/rate-limit"; import { prisma } from "@/lib/db"; import { auditLog } from "@/lib/auth/audit"; import { apiSuccess, getClientIp } from "@/lib/api-response"; @@ -8,6 +9,9 @@ import { annotate } from "@/lib/logging/context"; export const DELETE = apiHandler(async (request: NextRequest) => { const { user } = await requireAuth(); + const rl = await checkRateLimit(`codex-disconnect:${user.id}`, 5, 60_000); + if (!rl.allowed) throw new HttpError(429, "Too many requests"); + await prisma.user.update({ where: { id: user.id }, data: { diff --git a/src/lib/ai/codex-client.ts b/src/lib/ai/codex-client.ts index 0190be25f..016698e26 100644 --- a/src/lib/ai/codex-client.ts +++ b/src/lib/ai/codex-client.ts @@ -31,8 +31,7 @@ export class CodexClient implements AIProvider { } if (!firstAttempt.ok) { - const body = await firstAttempt.text(); - throw new Error(`Codex request failed (${firstAttempt.status}): ${body}`); + throw new Error(`Codex request failed (${firstAttempt.status})`); } return this.parseResponse(firstAttempt); diff --git a/src/lib/ai/codex-oauth.ts b/src/lib/ai/codex-oauth.ts index 8aaa30d37..bb3c8befe 100644 --- a/src/lib/ai/codex-oauth.ts +++ b/src/lib/ai/codex-oauth.ts @@ -53,8 +53,7 @@ export async function exchangeCodeForTokens(params: { }); if (!res.ok) { - const body = await res.text(); - throw new Error(`Codex token exchange failed (${res.status}): ${body}`); + throw new Error(`Codex token exchange failed (${res.status})`); } return res.json(); @@ -71,8 +70,7 @@ export async function refreshAccessToken(refreshToken: string): Promise { throw new Error("No refresh token available"); } - const { refreshToken: currentRefreshToken } = decryptTokens({ - accessEncrypted: "", - refreshEncrypted: freshUser.codexRefreshTokenEncrypted, - }); + const currentRefreshToken = decrypt(freshUser.codexRefreshTokenEncrypted); const newTokens = await refreshAccessToken(currentRefreshToken); const encrypted = encryptTokens({