codex-proxy / src /routes /admin /settings.ts
icebear
feat: dashboard login gate for public deployments (#148)
91ee702 unverified
raw
history blame
6.12 kB
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<string, unknown>).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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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;
}