fix: import the behavior of the web

This commit is contained in:
2026-07-12 20:28:25 +07:00
parent 83e7863fd9
commit 69927775ed
17 changed files with 234 additions and 88 deletions
@@ -26,7 +26,6 @@ export default function APIPageClient({ machineId, isAdmin }) {
const [confirmState, setConfirmState] = useState(null);
const [requireApiKey, setRequireApiKey] = useState(false);
const [requireLogin, setRequireLogin] = useState(true);
const [hasPassword, setHasPassword] = useState(true);
const [tunnelDashboardAccess, setTunnelDashboardAccess] = useState(false);
@@ -86,11 +85,9 @@ export default function APIPageClient({ machineId, isAdmin }) {
const { copied, copy } = useCopyToClipboard();
// Security gate: block remote exposure while dashboard uses default password or login is off.
const isLoginUnsafe = !requireLogin || !hasPassword;
const unsafeReason = !requireLogin
? "Enable \"Require login\" and set a custom password before activating the tunnel."
: "Change the default dashboard password before activating the tunnel.";
// Security gate: block remote exposure while dashboard uses the default password.
const isLoginUnsafe = !hasPassword;
const unsafeReason = "Change the default dashboard password before activating the tunnel.";
// Auto-scroll install log
useEffect(() => {
@@ -201,7 +198,6 @@ export default function APIPageClient({ machineId, isAdmin }) {
if (settingsRes.ok) {
const data = await settingsRes.json();
setRequireApiKey(data.requireApiKey || false);
setRequireLogin(data.requireLogin !== false);
setHasPassword(data.hasPassword || false);
setTunnelDashboardAccess(data.tunnelDashboardAccess || false);
}
@@ -925,15 +921,11 @@ export default function APIPageClient({ machineId, isAdmin }) {
action={{ label: "Enable", href: "#require-api-key" }}
/>
)}
{(!requireLogin || !hasPassword) && (
{!hasPassword && (
<SecurityWarning
message={
!requireLogin
? "Require login is disabled — anyone can access your dashboard via tunnel."
: "Dashboard uses the default password — change it in Profile settings."
}
message="Dashboard uses the default password — change it in Profile settings."
action={{
label: !requireLogin ? "Enable" : "Change password",
label: "Change password",
href: "/dashboard/profile",
}}
/>
+111 -1
View File
@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Card, Button, Input } from "@/shared/components";
import { Card, Button, Toggle, Input } from "@/shared/components";
import Modal, { ConfirmModal } from "@/shared/components/Modal";
import LanguageSwitcher from "@/shared/components/LanguageSwitcher";
import { useTheme } from "@/shared/hooks/useTheme";
@@ -23,6 +23,9 @@ function getLocaleFromCookie() {
export default function ProfilePage() {
const { theme, setTheme } = useTheme();
const user = useUserStore((state) => state.user);
// Only standard user accounts are restricted. Local mode has no dashboard
// session, so it must retain administrator-level settings controls.
const isAdmin = user?.role !== "user";
const fetchCurrentUser = useUserStore((state) => state.fetchCurrentUser);
const [locale, setLocale] = useState("en");
const [langOpen, setLangOpen] = useState(false);
@@ -37,7 +40,18 @@ export default function ProfilePage() {
const [dbStatus, setDbStatus] = useState({ type: "", message: "" });
const [dbAuth, setDbAuth] = useState({ open: false, mode: "", password: "" });
const pendingImportRef = useRef(null);
const [oidcForm, setOidcForm] = useState({ authMode: "password", oidcIssuerUrl: "", oidcClientId: "", oidcScopes: "openid profile email", oidcLoginLabel: "Sign in with OIDC" });
const [oidcClientSecret, setOidcClientSecret] = useState("");
const [oidcStatus, setOidcStatus] = useState({ type: "", message: "" });
const [oidcLoading, setOidcLoading] = useState(false);
const [oidcTestLoading, setOidcTestLoading] = useState(false);
const [oidcTestStatus, setOidcTestStatus] = useState({ type: "", message: "" });
const [oidcExpanded, setOidcExpanded] = useState(false);
const importFileRef = useRef(null);
const [proxyForm, setProxyForm] = useState({ outboundProxyUrl: "", outboundNoProxy: "" });
const [proxyStatus, setProxyStatus] = useState({ type: "", message: "" });
const [proxyLoading, setProxyLoading] = useState(false);
const [proxyTestLoading, setProxyTestLoading] = useState(false);
useEffect(() => {
if (!user) fetchCurrentUser();
@@ -52,6 +66,8 @@ export default function ProfilePage() {
.then((res) => res.json())
.then((data) => {
setSettings(data);
setOidcForm({ authMode: data?.authMode || "password", oidcIssuerUrl: data?.oidcIssuerUrl || "", oidcClientId: data?.oidcClientId || "", oidcScopes: data?.oidcScopes || "openid profile email", oidcLoginLabel: data?.oidcLoginLabel || "Sign in with OIDC" });
setProxyForm({ outboundProxyUrl: data?.outboundProxyUrl || "", outboundNoProxy: data?.outboundNoProxy || "" });
setLoading(false);
})
.catch((err) => {
@@ -60,6 +76,55 @@ export default function ProfilePage() {
});
}, []);
const updateAdminSettings = async (changes) => {
const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(changes) });
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to update settings");
setSettings((previous) => ({ ...previous, ...data }));
return data;
};
const saveOidcSettings = async () => {
setOidcLoading(true);
setOidcStatus({ type: "", message: "" });
try {
const payload = { ...oidcForm, oidcIssuerUrl: oidcForm.oidcIssuerUrl.trim(), oidcClientId: oidcForm.oidcClientId.trim(), oidcScopes: oidcForm.oidcScopes.trim() || "openid profile email", oidcLoginLabel: oidcForm.oidcLoginLabel.trim() || "Sign in with OIDC" };
if (oidcClientSecret.trim()) payload.oidcClientSecret = oidcClientSecret.trim();
const data = await updateAdminSettings(payload);
setOidcForm((previous) => ({ ...previous, authMode: data.authMode || previous.authMode }));
setOidcClientSecret("");
setOidcStatus({ type: "success", message: "OIDC settings saved" });
} catch (error) { setOidcStatus({ type: "error", message: error.message }); } finally { setOidcLoading(false); }
};
const testOidcConnection = async () => {
setOidcTestLoading(true);
setOidcTestStatus({ type: "", message: "" });
try {
const res = await fetch("/api/auth/oidc/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ issuerUrl: oidcForm.oidcIssuerUrl, clientId: oidcForm.oidcClientId, scopes: oidcForm.oidcScopes }) });
const data = await res.json();
if (!res.ok || !data.ok) throw new Error(data.error || "OIDC connection test failed");
setOidcTestStatus({ type: "success", message: "OIDC connection successful" });
} catch (error) { setOidcTestStatus({ type: "error", message: error.message }); } finally { setOidcTestLoading(false); }
};
const saveProxySettings = async (event) => {
event.preventDefault();
setProxyLoading(true);
try { await updateAdminSettings(proxyForm); setProxyStatus({ type: "success", message: "Proxy settings applied" }); }
catch (error) { setProxyStatus({ type: "error", message: error.message }); } finally { setProxyLoading(false); }
};
const testOutboundProxy = async () => {
setProxyTestLoading(true);
try {
const res = await fetch("/api/settings/proxy-test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ proxyUrl: proxyForm.outboundProxyUrl }) });
const data = await res.json();
if (!res.ok || !data.ok) throw new Error(data.error || "Proxy test failed");
setProxyStatus({ type: "success", message: "Proxy test successful" });
} catch (error) { setProxyStatus({ type: "error", message: error.message }); } finally { setProxyTestLoading(false); }
};
const handlePasswordChange = async (e) => {
e.preventDefault();
if (passwords.new !== passwords.confirm) {
@@ -315,6 +380,51 @@ export default function ProfilePage() {
</button>
</Card>
{isAdmin && (
<>
<Card>
<button type="button" onClick={() => setOidcExpanded((value) => !value)} className="w-full flex items-center gap-3 text-left">
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-500 shrink-0"><span className="material-symbols-outlined text-[20px]">lock_open</span></div>
<div className="flex-1"><h3 className="text-base sm:text-lg font-semibold">OIDC Dashboard Login</h3><p className="text-xs text-text-muted">Configure OIDC single sign-on for the dashboard.</p></div>
<span className="material-symbols-outlined text-text-muted">{oidcExpanded ? "expand_less" : "expand_more"}</span>
</button>
{oidcExpanded && (
<div className="flex flex-col gap-4 mt-4 pt-4 border-t border-border/50">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{["password", "oidc", "both"].map((mode) => <Button key={mode} type="button" variant={oidcForm.authMode === mode ? "primary" : "outline"} onClick={() => setOidcForm((previous) => ({ ...previous, authMode: mode }))}>{mode}</Button>)}
</div>
<Input placeholder="Issuer URL" value={oidcForm.oidcIssuerUrl} onChange={(event) => setOidcForm((previous) => ({ ...previous, oidcIssuerUrl: event.target.value }))} />
<Input placeholder="Client ID" value={oidcForm.oidcClientId} onChange={(event) => setOidcForm((previous) => ({ ...previous, oidcClientId: event.target.value }))} />
<Input type="password" placeholder="Client Secret (leave blank to keep)" value={oidcClientSecret} onChange={(event) => setOidcClientSecret(event.target.value)} />
<Input placeholder="Scopes" value={oidcForm.oidcScopes} onChange={(event) => setOidcForm((previous) => ({ ...previous, oidcScopes: event.target.value }))} />
<Input placeholder="Login button label" value={oidcForm.oidcLoginLabel} onChange={(event) => setOidcForm((previous) => ({ ...previous, oidcLoginLabel: event.target.value }))} />
<div className="flex gap-2"><Button type="button" variant="primary" loading={oidcLoading} onClick={saveOidcSettings}>Save</Button><Button type="button" variant="outline" loading={oidcTestLoading} onClick={testOidcConnection}>Test connection</Button></div>
{[oidcStatus, oidcTestStatus].filter((status) => status.message).map((status, index) => <p key={index} className={`text-sm ${status.type === "error" ? "text-red-500" : "text-green-500"}`}>{status.message}</p>)}
</div>
)}
</Card>
<Card>
<div className="flex items-center gap-3 mb-4"><div className="p-2 rounded-lg bg-blue-500/10 text-blue-500"><span className="material-symbols-outlined text-[20px]">route</span></div><h3 className="text-base sm:text-lg font-semibold">Routing Strategy</h3></div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4"><div><p className="font-medium">Round Robin</p><p className="text-xs text-text-muted">Cycle through accounts to distribute load.</p></div><Toggle checked={settings.fallbackStrategy === "round-robin"} onChange={() => updateAdminSettings({ fallbackStrategy: settings.fallbackStrategy === "round-robin" ? "fill-first" : "round-robin" })} /></div>
<div className="flex items-center justify-between gap-4"><div><p className="font-medium">Combo Round Robin</p><p className="text-xs text-text-muted">Cycle through providers in combos.</p></div><Toggle checked={settings.comboStrategy === "round-robin"} onChange={() => updateAdminSettings({ comboStrategy: settings.comboStrategy === "round-robin" ? "fallback" : "round-robin" })} /></div>
</div>
</Card>
<Card>
<div className="flex items-center gap-3 mb-4"><div className="p-2 rounded-lg bg-purple-500/10 text-purple-500"><span className="material-symbols-outlined text-[20px]">wifi</span></div><h3 className="text-base sm:text-lg font-semibold">Network</h3></div>
<div className="flex items-center justify-between gap-4 mb-4"><div><p className="font-medium">Outbound Proxy</p><p className="text-xs text-text-muted">Proxy OAuth and provider outbound requests.</p></div><Toggle checked={settings.outboundProxyEnabled === true} onChange={() => updateAdminSettings({ outboundProxyEnabled: !settings.outboundProxyEnabled })} /></div>
{settings.outboundProxyEnabled === true && <form onSubmit={saveProxySettings} className="flex flex-col gap-3 pt-4 border-t border-border/50"><Input placeholder="Proxy URL" value={proxyForm.outboundProxyUrl} onChange={(event) => setProxyForm((previous) => ({ ...previous, outboundProxyUrl: event.target.value }))} /><Input placeholder="No Proxy" value={proxyForm.outboundNoProxy} onChange={(event) => setProxyForm((previous) => ({ ...previous, outboundNoProxy: event.target.value }))} /><div className="flex gap-2"><Button type="button" variant="outline" loading={proxyTestLoading} onClick={testOutboundProxy}>Test proxy URL</Button><Button type="submit" variant="primary" loading={proxyLoading}>Apply</Button></div>{proxyStatus.message && <p className={`text-sm ${proxyStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>{proxyStatus.message}</p>}</form>}
</Card>
<Card>
<div className="flex items-center gap-3 mb-4"><div className="p-2 rounded-lg bg-orange-500/10 text-orange-500"><span className="material-symbols-outlined text-[20px]">monitoring</span></div><h3 className="text-base sm:text-lg font-semibold">Observability</h3></div>
<div className="flex items-center justify-between gap-4"><div><p className="font-medium">Enable Observability</p><p className="text-xs text-text-muted">Record request details for the logs view.</p></div><Toggle checked={settings.enableObservability === true} onChange={() => updateAdminSettings({ enableObservability: !settings.enableObservability })} /></div>
</Card>
</>
)}
{/* Password */}
<Card>
<div className="flex items-center gap-3 mb-4">
+28 -2
View File
@@ -1,4 +1,30 @@
import { NextResponse } from "next/server";
export async function POST() {
return NextResponse.json({ error: "OIDC settings are not available" }, { status: 403 });
import { getSettings } from "@/lib/localDb";
import { fetchOidcDiscovery, getPublicOrigin, probeOidcClientSecret } from "@/lib/auth/oidc";
import { requireUsageDashboardUser } from "@/lib/auth/currentUser";
export async function POST(request) {
try {
const user = await requireUsageDashboardUser();
if (user.role !== "admin") throw new Error("Forbidden");
const body = await request.json().catch(() => ({}));
const settings = await getSettings();
const issuerUrl = String(body.issuerUrl || settings.oidcIssuerUrl || "").trim();
const clientId = String(body.clientId || settings.oidcClientId || "").trim();
const scopes = String(body.scopes || settings.oidcScopes || "openid profile email").trim() || "openid profile email";
const clientSecret = String(body.clientSecret || settings.oidcClientSecret || "").trim();
if (!issuerUrl || !clientId) return NextResponse.json({ error: "Issuer URL and client ID are required" }, { status: 400 });
const discovery = await fetchOidcDiscovery(issuerUrl);
const redirectUri = `${getPublicOrigin(request)}/api/auth/oidc/callback`;
const secretProbe = await probeOidcClientSecret({
tokenEndpoint: discovery.token_endpoint,
clientId,
clientSecret,
redirectUri,
});
return NextResponse.json({ ok: secretProbe.valid !== false, discoveryOk: true, clientSecretTested: secretProbe.tested, clientSecretValid: secretProbe.valid, issuerUrl, clientId, scopes, message: secretProbe.message });
} catch (error) {
const status = error.message === "Unauthorized" ? 401 : error.message === "Forbidden" ? 403 : 500;
return NextResponse.json({ error: error.message || "OIDC test failed" }, { status });
}
}
-3
View File
@@ -9,7 +9,6 @@ export async function GET() {
const settings = await getSettings();
const cookieStore = await cookies();
const session = await getDashboardAuthSession(cookieStore.get("auth_token")?.value);
const requireLogin = settings.requireLogin !== false;
const authMode = settings.authMode || "password";
const oidcName = String(session?.oidcName || "").trim();
const oidcEmail = String(session?.oidcEmail || "").trim();
@@ -20,7 +19,6 @@ export async function GET() {
const loginMethod = session?.oidc ? "OIDC" : "Password";
return NextResponse.json({
requireLogin,
authMode,
oidcConfigured: isOidcConfigured(settings),
oidcLoginLabel: (settings.oidcLoginLabel || "Sign in with OIDC").trim() || "Sign in with OIDC",
@@ -36,7 +34,6 @@ export async function GET() {
});
} catch {
return NextResponse.json({
requireLogin: true,
authMode: "password",
oidcConfigured: false,
oidcLoginLabel: "Sign in with OIDC",
+27 -2
View File
@@ -1,6 +1,31 @@
import { NextResponse } from "next/server";
import { getComboById, updateComboStrategy } from "@/lib/localDb";
import { requireUsageDashboardUser } from "@/lib/auth/currentUser";
import { resetComboRotation } from "open-sse/services/combo.js";
export const dynamic = "force-dynamic";
export async function PATCH() {
return NextResponse.json({ error: "Routing strategy settings are not available" }, { status: 403 });
const STRATEGIES = new Set(["fallback", "round-robin", "fusion"]);
export async function PATCH(request, { params }) {
try {
const user = await requireUsageDashboardUser();
if (user.role !== "admin") throw new Error("Forbidden");
const { id } = await params;
const combo = await getComboById(id);
if (!combo) return NextResponse.json({ error: "Combo not found" }, { status: 404 });
const { strategy } = await request.json();
if (!strategy || typeof strategy !== "object" || Array.isArray(strategy)) return NextResponse.json({ error: "Strategy must be an object" }, { status: 400 });
if (strategy.fallbackStrategy !== undefined && !STRATEGIES.has(strategy.fallbackStrategy)) return NextResponse.json({ error: "Invalid combo strategy" }, { status: 400 });
if (strategy.judgeModel !== undefined && (typeof strategy.judgeModel !== "string" || strategy.judgeModel.length > 256)) return NextResponse.json({ error: "Invalid combo strategy" }, { status: 400 });
const normalizedStrategy = {};
if (strategy.fallbackStrategy !== undefined) normalizedStrategy.fallbackStrategy = strategy.fallbackStrategy;
if (strategy.judgeModel !== undefined) normalizedStrategy.judgeModel = strategy.judgeModel.trim();
const settings = await updateComboStrategy(combo.id, normalizedStrategy);
resetComboRotation(combo.id);
return NextResponse.json({ strategy: settings.comboStrategies[combo.id] || {} });
} catch (error) {
const status = error.message === "Unauthorized" ? 401 : error.message === "Forbidden" ? 403 : 500;
return NextResponse.json({ error: error.message || "Failed to update combo strategy" }, { status });
}
}
+15 -2
View File
@@ -1,4 +1,17 @@
import { NextResponse } from "next/server";
export async function POST() {
return NextResponse.json({ error: "Network settings are not available" }, { status: 403 });
import { testProxyUrl } from "@/lib/network/proxyTest";
import { requireUsageDashboardUser } from "@/lib/auth/currentUser";
export async function POST(request) {
try {
const user = await requireUsageDashboardUser();
if (user.role !== "admin") throw new Error("Forbidden");
const body = await request.json();
const result = await testProxyUrl({ proxyUrl: body?.proxyUrl, testUrl: body?.testUrl, timeoutMs: body?.timeoutMs });
if (result?.ok) return NextResponse.json(result);
return NextResponse.json({ ok: false, error: result?.error || "Proxy test failed" }, { status: result?.status || 500 });
} catch (error) {
const status = error.message === "Unauthorized" ? 401 : error.message === "Forbidden" ? 403 : 500;
return NextResponse.json({ ok: false, error: error.message || "Proxy test failed" }, { status });
}
}
@@ -1,4 +0,0 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ error: "Login settings are not available" }, { status: 403 });
}
+17 -9
View File
@@ -16,17 +16,13 @@ const SETTINGS_RESPONSE_HEADERS = {
// Secrets must never be mass-assigned from request body (CWE-915)
const PROTECTED_SETTING_KEYS = ["password", "mitmSudoEncrypted"];
// These capabilities are intentionally not configurable through the dashboard.
// Keep the server-side policy here so callers cannot bypass the hidden UI.
const RESTRICTED_SETTING_KEYS = [
"requireLogin",
const USER_RESTRICTED_SETTING_KEYS = [
"authMode",
"oidcIssuerUrl",
"oidcClientId",
"oidcClientSecret",
"oidcScopes",
"oidcLoginLabel",
"oidcConfigured",
"fallbackStrategy",
"stickyRoundRobinLimit",
"comboStrategy",
@@ -60,9 +56,9 @@ export async function GET() {
try {
const settings = await getSettings();
const { password, oidcClientSecret, ...safeSettings } = settings;
for (const key of RESTRICTED_SETTING_KEYS) delete safeSettings[key];
const user = await requireUsageDashboardUser();
if (user.role !== "admin") {
for (const key of USER_RESTRICTED_SETTING_KEYS) delete safeSettings[key];
const ownedComboIds = new Set((await getCombos(user.id)).map((combo) => combo.id));
safeSettings.comboStrategies = Object.fromEntries(
Object.entries(safeSettings.comboStrategies || {}).filter(([comboId]) => ownedComboIds.has(comboId))
@@ -88,8 +84,16 @@ export async function PATCH(request) {
try {
const body = await request.json();
if (RESTRICTED_SETTING_KEYS.some((key) => Object.prototype.hasOwnProperty.call(body, key))) {
return NextResponse.json({ error: "This setting is not available" }, { status: 403 });
if (USER_RESTRICTED_SETTING_KEYS.some((key) => Object.prototype.hasOwnProperty.call(body, key))) {
let user;
try {
user = await requireUsageDashboardUser();
} catch {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (user.role !== "admin") {
return NextResponse.json({ error: "Administrator access required" }, { status: 403 });
}
}
if (
@@ -168,7 +172,11 @@ export async function PATCH(request) {
}
const { password, oidcClientSecret, ...safeSettings } = settings;
for (const key of RESTRICTED_SETTING_KEYS) delete safeSettings[key];
const user = await requireUsageDashboardUser();
if (user.role !== "admin") {
for (const key of USER_RESTRICTED_SETTING_KEYS) delete safeSettings[key];
}
safeSettings.oidcConfigured = !!(safeSettings.oidcIssuerUrl && safeSettings.oidcClientId && oidcClientSecret);
return NextResponse.json(safeSettings, { headers: SETTINGS_RESPONSE_HEADERS });
} catch (error) {
console.log("Error updating settings:", error);
-4
View File
@@ -38,10 +38,6 @@ export default function LoginPage() {
if (res.ok) {
const data = await res.json();
if (data.requireLogin === false) {
window.location.assign("/dashboard");
return;
}
setHasPassword(!!data.hasPassword);
setAuthMode(data.authMode || "password");
setOidcConfigured(data.oidcConfigured === true);
+5 -13
View File
@@ -33,7 +33,7 @@ const PUBLIC_API_PATHS = [
// Public top-level prefixes (LLM API endpoints with their own API key auth).
const PUBLIC_PREFIXES = ["/v1", "/v1beta", "/api/v1", "/api/v1beta", "/codex"];
// Always require JWT token regardless of requireLogin setting
// Always require a JWT token.
const ALWAYS_PROTECTED = [
"/api/shutdown",
"/api/settings/database",
@@ -69,7 +69,7 @@ const ADMIN_ONLY_DASHBOARD_PATHS = [
"/dashboard/console-log",
];
// Require auth, but allow through if requireLogin is disabled
// Require authenticated access.
const PROTECTED_API_PATHS = [
"/api/settings",
"/api/keys",
@@ -164,7 +164,7 @@ async function canAccessPublicLlmApi(request) {
async function canAccessLocalOnlyRoute(request) {
if (await hasValidCliToken(request)) return true;
// Browser on host: loopback Host + Origin (blocks tunnel/CSRF) + auth (JWT or requireLogin=false)
// Browser on host: loopback Host + Origin (blocks tunnel/CSRF) + JWT auth.
if (isLocalRequest(request) && await isAuthenticated(request)) return true;
return false;
}
@@ -202,10 +202,7 @@ async function loadSettings() {
}
async function isAuthenticated(request) {
if (await hasValidToken(request)) return true;
const settings = await loadSettings();
if (settings && settings.requireLogin === false) return true;
return false;
return hasValidToken(request);
}
function isPublicApi(pathname) {
@@ -277,13 +274,11 @@ export async function proxy(request) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
let requireLogin = true;
let tunnelDashboardAccess = true;
try {
const settings = await loadSettings();
if (settings) {
requireLogin = settings.requireLogin !== false;
tunnelDashboardAccess = settings.tunnelDashboardAccess === true;
// Block tunnel/tailscale access if disabled (redirect to login)
@@ -297,12 +292,9 @@ export async function proxy(request) {
}
}
} catch {
// On error, keep defaults (require login, block tunnel)
// On error, keep the secure default and block tunnel dashboard access.
}
// If login not required, allow through
if (!requireLogin) return NextResponse.next();
// Verify JWT token
const token = request.cookies.get("auth_token")?.value;
if (token) {
+2 -6
View File
@@ -1,6 +1,6 @@
import { cookies } from "next/headers";
import { getDashboardAuthSession } from "./dashboardSession.js";
import { getSettings, getUserById, verifyUserPassword } from "@/lib/db";
import { getUserById, verifyUserPassword } from "@/lib/db";
export async function getCurrentDashboardUser() {
const cookieStore = await cookies();
@@ -23,16 +23,12 @@ export async function requireCurrentDashboardUser() {
}
/**
* Resolve the user for dashboard data that is also available in the explicit
* single-user (`requireLogin=false`) deployment mode. That mode has no account
* boundary, so it intentionally uses the system-wide administrator scope.
* Resolve the currently authenticated dashboard user.
*/
export async function requireUsageDashboardUser() {
const user = await getCurrentDashboardUser();
if (user) return user;
const settings = await getSettings();
if (settings?.requireLogin === false) return { id: null, username: "local", role: "admin" };
throw new Error("Unauthorized");
}
+4 -11
View File
@@ -6,21 +6,16 @@ const DEFAULT_MAX_RECORDS = 200;
const DEFAULT_BATCH_SIZE = 20;
const DEFAULT_FLUSH_INTERVAL_MS = 5000;
const DEFAULT_MAX_JSON_SIZE = 5 * 1024;
const CONFIG_CACHE_TTL_MS = 5000;
let cachedConfig = null;
let cachedConfigTs = 0;
async function getObservabilityConfig() {
if (cachedConfig && (Date.now() - cachedConfigTs) < CONFIG_CACHE_TTL_MS) return cachedConfig;
try {
const { getSettings } = await import("./settingsRepo.js");
const settings = await getSettings();
const envEnabled = process.env.OBSERVABILITY_ENABLED !== "false";
const enabled = typeof settings.enableObservability2 === "boolean"
? settings.enableObservability2
const enabled = typeof settings.enableObservability === "boolean"
? settings.enableObservability
: envEnabled;
cachedConfig = {
return {
enabled,
maxRecords: settings.observabilityMaxRecords || parseInt(process.env.OBSERVABILITY_MAX_RECORDS || String(DEFAULT_MAX_RECORDS), 10),
batchSize: settings.observabilityBatchSize || parseInt(process.env.OBSERVABILITY_BATCH_SIZE || String(DEFAULT_BATCH_SIZE), 10),
@@ -28,7 +23,7 @@ async function getObservabilityConfig() {
maxJsonSize: (settings.observabilityMaxJsonSize || parseInt(process.env.OBSERVABILITY_MAX_JSON_SIZE || "5", 10)) * 1024,
};
} catch {
cachedConfig = {
return {
enabled: false,
maxRecords: DEFAULT_MAX_RECORDS,
batchSize: DEFAULT_BATCH_SIZE,
@@ -36,8 +31,6 @@ async function getObservabilityConfig() {
maxJsonSize: DEFAULT_MAX_JSON_SIZE,
};
}
cachedConfigTs = Date.now();
return cachedConfig;
}
let writeBuffer = [];
-1
View File
@@ -16,7 +16,6 @@ const DEFAULT_SETTINGS = {
comboStrategy: "fallback",
comboStickyRoundRobinLimit: 1,
comboStrategies: {},
requireLogin: true,
tunnelDashboardAccess: true,
authMode: "password",
oidcIssuerUrl: "",
+7
View File
@@ -13,6 +13,13 @@ export async function getUsageAccessScope(user) {
return { isAdmin: true, userId: null, connectionIds: [], apiKeys: [] };
}
// Repository calls without a user are trusted server-side operations (for
// example logging, cleanup, and DB-level callers). HTTP routes must resolve
// and pass the dashboard user explicitly before querying these repositories.
if (!user) {
return { isAdmin: true, userId: null, connectionIds: [], apiKeys: [] };
}
if (!user?.id) {
return { isAdmin: false, userId: null, connectionIds: [], apiKeys: [] };
}