This commit is contained in:
decolua
2026-05-12 09:54:58 +07:00
parent c3d91b019b
commit 067b7c5292
4 changed files with 50 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "9router-app", "name": "9router-app",
"version": "0.4.30", "version": "0.4.31",
"description": "9Router web dashboard", "description": "9Router web dashboard",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@@ -28,6 +28,7 @@ export default function ProfilePage() {
const [oidcTestLoading, setOidcTestLoading] = useState(false); const [oidcTestLoading, setOidcTestLoading] = useState(false);
const [oidcTestStatus, setOidcTestStatus] = useState({ type: "", message: "" }); const [oidcTestStatus, setOidcTestStatus] = useState({ type: "", message: "" });
const [oidcRedirectUri, setOidcRedirectUri] = useState("/api/auth/oidc/callback"); const [oidcRedirectUri, setOidcRedirectUri] = useState("/api/auth/oidc/callback");
const [oidcExpanded, setOidcExpanded] = useState(false);
const importFileRef = useRef(null); const importFileRef = useRef(null);
const [proxyForm, setProxyForm] = useState({ const [proxyForm, setProxyForm] = useState({
outboundProxyEnabled: false, outboundProxyEnabled: false,
@@ -51,6 +52,7 @@ export default function ProfilePage() {
oidcLoginLabel: data?.oidcLoginLabel || "Sign in with OIDC", oidcLoginLabel: data?.oidcLoginLabel || "Sign in with OIDC",
}); });
setOidcClientSecret(""); setOidcClientSecret("");
if (data?.authMode === "oidc" || data?.authMode === "both") setOidcExpanded(true);
setProxyForm({ setProxyForm({
outboundProxyEnabled: data?.outboundProxyEnabled === true, outboundProxyEnabled: data?.outboundProxyEnabled === true,
outboundProxyUrl: data?.outboundProxyUrl || "", outboundProxyUrl: data?.outboundProxyUrl || "",
@@ -675,13 +677,26 @@ export default function ProfilePage() {
{/* OIDC */} {/* OIDC */}
<Card> <Card>
<div className="flex items-center gap-3 mb-4"> <button
type="button"
onClick={() => setOidcExpanded((v) => !v)}
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"> <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> <span className="material-symbols-outlined text-[20px]">lock_open</span>
</div> </div>
<h3 className="text-base sm:text-lg font-semibold">OIDC Dashboard Login</h3> <div className="flex-1 min-w-0">
</div> <h3 className="text-base sm:text-lg font-semibold">OIDC Dashboard Login</h3>
<div className="flex flex-col gap-4"> <p className="text-xs text-text-muted">
{settings.authMode === "oidc" ? "OIDC active" : settings.authMode === "both" ? "Password + OIDC active" : "Optional SSO via Authentik/Keycloak/Google"}
</p>
</div>
<span className="material-symbols-outlined text-text-muted shrink-0">
{oidcExpanded ? "expand_less" : "expand_more"}
</span>
</button>
{oidcExpanded && (
<div className="flex flex-col gap-4 mt-4">
<p className="text-xs sm:text-sm text-text-muted"> <p className="text-xs sm:text-sm text-text-muted">
Use Authentik or any OIDC provider to sign in to the dashboard. You can enable password-only, OIDC-only, or both for the dashboard; model API access still uses API keys. Use Authentik or any OIDC provider to sign in to the dashboard. You can enable password-only, OIDC-only, or both for the dashboard; model API access still uses API keys.
</p> </p>
@@ -820,6 +835,7 @@ export default function ProfilePage() {
</p> </p>
)} )}
</div> </div>
)}
</Card> </Card>
{/* Routing Preferences */} {/* Routing Preferences */}

View File

@@ -252,46 +252,35 @@ export default function ProvidersPage() {
} }
}; };
const compatibleProviders = sortItemsByPriority( const compatibleProviders = providerNodes
providerNodes .filter((node) => node.type === "openai-compatible")
.filter((node) => node.type === "openai-compatible") .map((node) => ({
.map((node) => ({ id: node.id,
id: node.id, name: node.name || "OpenAI Compatible",
name: node.name || "OpenAI Compatible", color: "#10A37F",
color: "#10A37F", textIcon: "OC",
textIcon: "OC", apiType: node.apiType,
apiType: node.apiType, }))
})) .filter((p) => matchSearch(p.name));
.filter((p) => matchSearch(p.name)),
"apikey",
);
const anthropicCompatibleProviders = sortItemsByPriority( const anthropicCompatibleProviders = providerNodes
providerNodes .filter((node) => node.type === "anthropic-compatible")
.filter((node) => node.type === "anthropic-compatible") .map((node) => ({
.map((node) => ({ id: node.id,
id: node.id, name: node.name || "Anthropic Compatible",
name: node.name || "Anthropic Compatible", color: "#D97757",
color: "#D97757", textIcon: "AC",
textIcon: "AC", }))
})) .filter((p) => matchSearch(p.name));
.filter((p) => matchSearch(p.name)),
"apikey",
);
const oauthEntries = sortByPriority( const oauthEntries = Object.entries(OAUTH_PROVIDERS).filter(([, info]) =>
Object.entries(OAUTH_PROVIDERS).filter(([, info]) => matchSearch(info.name)), matchSearch(info.name),
"oauth",
); );
const freeEntries = sortByPriority( const freeEntries = Object.entries(FREE_PROVIDERS).filter(([, info]) =>
Object.entries(FREE_PROVIDERS).filter(([, info]) => matchSearch(info.name)), matchSearch(info.name),
"oauth",
); );
const freeTierEntries = sortByPriority( const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS).filter(
Object.entries(FREE_TIER_PROVIDERS).filter(([, info]) => ([, info]) => matchSearch(info.name),
matchSearch(info.name),
),
"apikey",
); );
const apikeyEntries = sortByPriority( const apikeyEntries = sortByPriority(
Object.entries(APIKEY_PROVIDERS).filter( Object.entries(APIKEY_PROVIDERS).filter(

View File

@@ -294,15 +294,13 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
{/* Right actions */} {/* Right actions */}
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
{displayName && ( {displayName && loginMethod === "OIDC" && (
<div className="hidden sm:flex items-center max-w-[220px] px-3 py-1.5 rounded-full border border-border bg-surface/70 text-xs text-text-muted truncate"> <div className="hidden sm:flex items-center max-w-[220px] px-3 py-1.5 rounded-full border border-border bg-surface/70 text-xs text-text-muted truncate">
<span className="material-symbols-outlined text-[14px] mr-1.5 text-primary">person</span> <span className="material-symbols-outlined text-[14px] mr-1.5 text-primary">person</span>
<span className="truncate">{displayName}</span> <span className="truncate">{displayName}</span>
{loginMethod && ( <span className="ml-2 shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary">
<span className="ml-2 shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary"> OIDC
{loginMethod} </span>
</span>
)}
</div> </div>
)} )}
<HeaderSearch /> <HeaderSearch />