diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 73c94867d1..ce09e28a46 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -276,7 +276,7 @@ export default function ProviderDetailPage() { const handleToggleRateLimit = async (connectionId, enabled) => { try { - const res = await fetch("/api/rate-limit", { + const res = await fetch("/api/rate-limits", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ connectionId, enabled }), diff --git a/src/app/api/rate-limit/route.js b/src/app/api/rate-limit/route.js index e4f247b8f1..4be337aaa9 100644 --- a/src/app/api/rate-limit/route.js +++ b/src/app/api/rate-limit/route.js @@ -1,63 +1,18 @@ import { NextResponse } from "next/server"; -import { getProviderConnections, updateProviderConnection } from "@/lib/localDb"; -import { - enableRateLimitProtection, - disableRateLimitProtection, - getRateLimitStatus, - getAllRateLimitStatus, -} from "@omniroute/open-sse/services/rateLimitManager.js"; /** - * GET /api/rate-limit — Get rate limit status for all connections + * @deprecated Use /api/rate-limits instead. + * This route redirects to the consolidated rate-limits endpoint. */ -export async function GET() { - try { - const connections = await getProviderConnections(); - const statuses = connections.map((conn) => ({ - connectionId: conn.id, - provider: conn.provider, - name: conn.name || conn.email || conn.id.slice(0, 8), - rateLimitProtection: !!conn.rateLimitProtection, - ...getRateLimitStatus(conn.provider, conn.id), - })); - return NextResponse.json({ - connections: statuses, - overview: getAllRateLimitStatus(), - }); - } catch (error) { - console.error("[API ERROR] /api/rate-limit GET:", error); - return NextResponse.json({ error: "Failed to get rate limit status" }, { status: 500 }); - } +export async function GET(request) { + const url = new URL(request.url); + url.pathname = "/api/rate-limits"; + return NextResponse.redirect(url, 308); } -/** - * POST /api/rate-limit — Toggle rate limit protection for a connection - * Body: { connectionId: string, enabled: boolean } - */ export async function POST(request) { - try { - const { connectionId, enabled } = await request.json(); - - if (!connectionId) { - return NextResponse.json({ error: "Missing connectionId" }, { status: 400 }); - } - - // Update in-memory state - if (enabled) { - enableRateLimitProtection(connectionId); - } else { - disableRateLimitProtection(connectionId); - } - - // Persist to database - await updateProviderConnection(connectionId, { - rateLimitProtection: !!enabled, - }); - - return NextResponse.json({ success: true, connectionId, enabled: !!enabled }); - } catch (error) { - console.error("[API ERROR] /api/rate-limit POST:", error); - return NextResponse.json({ error: "Failed to toggle rate limit" }, { status: 500 }); - } + const url = new URL(request.url); + url.pathname = "/api/rate-limits"; + return NextResponse.redirect(url, 308); } diff --git a/src/app/api/rate-limits/route.js b/src/app/api/rate-limits/route.js index 51c97e2308..887c8e61db 100644 --- a/src/app/api/rate-limits/route.js +++ b/src/app/api/rate-limits/route.js @@ -1,15 +1,76 @@ import { NextResponse } from "next/server"; -import { - getAllModelLockouts, -} from "@omniroute/open-sse/services/accountFallback.js"; +import { getAllModelLockouts } from "@omniroute/open-sse/services/accountFallback.js"; import { getCacheStats } from "@omniroute/open-sse/services/signatureCache.js"; +import { getProviderConnections, updateProviderConnection } from "@/lib/localDb"; +import { + enableRateLimitProtection, + disableRateLimitProtection, + getRateLimitStatus, + getAllRateLimitStatus, +} from "@omniroute/open-sse/services/rateLimitManager.js"; +/** + * GET /api/rate-limits — Consolidated rate-limit status + * + * Returns: + * - Per-connection rate-limit status (protection toggle, current state) + * - Global overview (all providers) + * - Model lockouts + * - Signature cache stats + */ export async function GET() { try { + const connections = await getProviderConnections(); + const statuses = connections.map((conn) => ({ + connectionId: conn.id, + provider: conn.provider, + name: conn.name || conn.email || conn.id.slice(0, 8), + rateLimitProtection: !!conn.rateLimitProtection, + ...getRateLimitStatus(conn.provider, conn.id), + })); + const lockouts = getAllModelLockouts(); const cacheStats = getCacheStats(); - return NextResponse.json({ lockouts, cacheStats }); + + return NextResponse.json({ + connections: statuses, + overview: getAllRateLimitStatus(), + lockouts, + cacheStats, + }); } catch (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); + console.error("[API ERROR] /api/rate-limits GET:", error); + return NextResponse.json({ error: "Failed to get rate limit status" }, { status: 500 }); + } +} + +/** + * POST /api/rate-limits — Toggle rate limit protection for a connection + * Body: { connectionId: string, enabled: boolean } + */ +export async function POST(request) { + try { + const { connectionId, enabled } = await request.json(); + + if (!connectionId) { + return NextResponse.json({ error: "Missing connectionId" }, { status: 400 }); + } + + // Update in-memory state + if (enabled) { + enableRateLimitProtection(connectionId); + } else { + disableRateLimitProtection(connectionId); + } + + // Persist to database + await updateProviderConnection(connectionId, { + rateLimitProtection: !!enabled, + }); + + return NextResponse.json({ success: true, connectionId, enabled: !!enabled }); + } catch (error) { + console.error("[API ERROR] /api/rate-limits POST:", error); + return NextResponse.json({ error: "Failed to toggle rate limit" }, { status: 500 }); } } diff --git a/src/app/global-error.js b/src/app/global-error.js index 4037f86a66..db7051860d 100644 --- a/src/app/global-error.js +++ b/src/app/global-error.js @@ -6,88 +6,29 @@ * Root-level error boundary for unrecoverable errors. * This is the last resort — catches errors that the per-page * error.js boundaries don't handle. + * Styled with TailwindCSS 4 (Phase 7.3). */ export default function GlobalError({ error, reset }) { return ( - -
- ⚠️ -
-

- Something went wrong -

-

+ +

⚠️
+

Something went wrong

+

An unexpected error occurred. This has been logged and our team will investigate.

{process.env.NODE_ENV === "development" && error?.message && ( -
+          
             {error.message}
           
)} diff --git a/src/app/not-found.js b/src/app/not-found.js index 6328f42ca7..5707611146 100644 --- a/src/app/not-found.js +++ b/src/app/not-found.js @@ -4,70 +4,33 @@ * Custom Not Found Page — FASE-04 Error Handling * * Displayed when a user navigates to a non-existent route. + * Styled with TailwindCSS 4 (Phase 7.3). */ import Link from "next/link"; export default function NotFound() { return ( -
+
404
-

- Page not found -

-

+

Page not found

+

The page you're looking for doesn't exist or has been moved.

Go to Dashboard diff --git a/src/app/privacy/page.js b/src/app/privacy/page.js index 5beb3e3779..6972a482f2 100644 --- a/src/app/privacy/page.js +++ b/src/app/privacy/page.js @@ -145,7 +145,7 @@ export default function PrivacyPage() {

Questions? Visit our{" "} { // Providers that support usage/quota API export const USAGE_SUPPORTED_PROVIDERS = ["antigravity", "kiro", "github", "codex", "claude"]; + +// ── Zod validation at module load (Phase 7.2) ── +import { validateProviders } from "../validation/providerSchema.js"; + +validateProviders(FREE_PROVIDERS, "FREE_PROVIDERS"); +validateProviders(OAUTH_PROVIDERS, "OAUTH_PROVIDERS"); +validateProviders(APIKEY_PROVIDERS, "APIKEY_PROVIDERS"); diff --git a/src/shared/validation/providerSchema.js b/src/shared/validation/providerSchema.js new file mode 100644 index 0000000000..4d90e213f0 --- /dev/null +++ b/src/shared/validation/providerSchema.js @@ -0,0 +1,38 @@ +/** + * Provider Schema Validation — Phase 7.2 + * + * Zod schemas for provider constant validation. + * Validates FREE_PROVIDERS, OAUTH_PROVIDERS, and APIKEY_PROVIDERS + * at module load time to catch configuration drift early. + * + * @module shared/validation/providerSchema + */ + +import { z } from "zod"; + +export const ProviderSchema = z.object({ + id: z.string().min(1), + alias: z.string().min(1), + name: z.string().min(1), + icon: z.string().min(1), + color: z.string().regex(/^#[0-9A-Fa-f]{6}$/, "Must be a valid hex color (#RRGGBB)"), + textIcon: z.string().optional(), + website: z.string().url().optional(), + passthroughModels: z.boolean().optional(), +}); + +export const ProvidersMapSchema = z.record(z.string(), ProviderSchema); + +/** + * Validate a providers map, throwing a descriptive error on failure. + * @param {Record} map - The providers map to validate + * @param {string} name - Name of the map for error messages + */ +export function validateProviders(map, name) { + const result = ProvidersMapSchema.safeParse(map); + if (!result.success) { + const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n"); + console.error(`[PROVIDER VALIDATION] ${name} has invalid entries:\n${issues}`); + throw new Error(`Provider validation failed for ${name}`); + } +}