import { Hono } from "hono"; import { resolve } from "path"; import { getConnInfo } from "@hono/node-server/conninfo"; import { getConfig, reloadAllConfigs, ROTATION_STRATEGIES } from "../../config.js"; import { getConfigDir } from "../../paths.js"; import { mutateYaml } from "../../utils/yaml-mutate.js"; import { isLocalhostRequest } from "../../utils/is-localhost.js"; export function createSettingsRoutes(): Hono { const app = new Hono(); // --- Rotation settings --- app.get("/admin/rotation-settings", (c) => { const config = getConfig(); return c.json({ rotation_strategy: config.auth.rotation_strategy, }); }); app.post("/admin/rotation-settings", async (c) => { const config = getConfig(); const currentKey = config.server.proxy_api_key; if (currentKey) { const authHeader = c.req.header("Authorization") ?? ""; const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : ""; if (token !== currentKey) { c.status(401); return c.json({ error: "Invalid current API key" }); } } const body = await c.req.json() as { rotation_strategy?: string }; const valid: readonly string[] = ROTATION_STRATEGIES; if (!body.rotation_strategy || !valid.includes(body.rotation_strategy)) { c.status(400); return c.json({ error: `rotation_strategy must be one of: ${ROTATION_STRATEGIES.join(", ")}` }); } const configPath = resolve(getConfigDir(), "default.yaml"); mutateYaml(configPath, (data) => { if (!data.auth) data.auth = {}; (data.auth as Record).rotation_strategy = body.rotation_strategy; }); reloadAllConfigs(); const updated = getConfig(); return c.json({ success: true, rotation_strategy: updated.auth.rotation_strategy, }); }); // --- General settings --- app.get("/admin/settings", (c) => { const config = getConfig(); return c.json({ proxy_api_key: config.server.proxy_api_key }); }); app.post("/admin/settings", async (c) => { const config = getConfig(); const currentKey = config.server.proxy_api_key; if (currentKey) { const authHeader = c.req.header("Authorization") ?? ""; const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : ""; if (token !== currentKey) { c.status(401); return c.json({ error: "Invalid current API key" }); } } const body = await c.req.json() as { proxy_api_key?: string | null }; const newKey = body.proxy_api_key === undefined ? currentKey : (body.proxy_api_key || null); // Prevent remote sessions from clearing the key (would disable login gate) if (currentKey && !newKey) { const remoteAddr = getConnInfo(c).remote.address ?? ""; if (!isLocalhostRequest(remoteAddr)) { c.status(403); return c.json({ error: "Cannot clear API key from remote session — this would disable the login gate" }); } } const configPath = resolve(getConfigDir(), "default.yaml"); mutateYaml(configPath, (data) => { const server = data.server as Record; server.proxy_api_key = newKey; }); reloadAllConfigs(); return c.json({ success: true, proxy_api_key: newKey }); }); // --- Quota settings --- app.get("/admin/quota-settings", (c) => { const config = getConfig(); return c.json({ refresh_interval_minutes: config.quota.refresh_interval_minutes, warning_thresholds: config.quota.warning_thresholds, skip_exhausted: config.quota.skip_exhausted, }); }); app.post("/admin/quota-settings", async (c) => { const config = getConfig(); const currentKey = config.server.proxy_api_key; if (currentKey) { const authHeader = c.req.header("Authorization") ?? ""; const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : ""; if (token !== currentKey) { c.status(401); return c.json({ error: "Invalid current API key" }); } } const body = await c.req.json() as { refresh_interval_minutes?: number; warning_thresholds?: { primary?: number[]; secondary?: number[] }; skip_exhausted?: boolean; }; if (body.refresh_interval_minutes !== undefined) { if (!Number.isInteger(body.refresh_interval_minutes) || body.refresh_interval_minutes < 1) { c.status(400); return c.json({ error: "refresh_interval_minutes must be an integer >= 1" }); } } const validateThresholds = (arr?: number[]): boolean => { if (!arr) return true; return arr.every((v) => Number.isInteger(v) && v >= 1 && v <= 100); }; if (body.warning_thresholds) { if (!validateThresholds(body.warning_thresholds.primary) || !validateThresholds(body.warning_thresholds.secondary)) { c.status(400); return c.json({ error: "Thresholds must be integers between 1 and 100" }); } } const configPath = resolve(getConfigDir(), "default.yaml"); mutateYaml(configPath, (data) => { if (!data.quota) data.quota = {}; const quota = data.quota as Record; if (body.refresh_interval_minutes !== undefined) { quota.refresh_interval_minutes = body.refresh_interval_minutes; } if (body.warning_thresholds) { const existing = (quota.warning_thresholds ?? {}) as Record; if (body.warning_thresholds.primary) existing.primary = body.warning_thresholds.primary; if (body.warning_thresholds.secondary) existing.secondary = body.warning_thresholds.secondary; quota.warning_thresholds = existing; } if (body.skip_exhausted !== undefined) { quota.skip_exhausted = body.skip_exhausted; } }); reloadAllConfigs(); const updated = getConfig(); return c.json({ success: true, refresh_interval_minutes: updated.quota.refresh_interval_minutes, warning_thresholds: updated.quota.warning_thresholds, skip_exhausted: updated.quota.skip_exhausted, }); }); return app; }