From 06fe172beaf6798af69f3f9efcba3b82155f2857 Mon Sep 17 00:00:00 2001 From: Yunling Zhang Date: Tue, 29 Sep 2026 13:44:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20admin=20=E9=A1=B5=E6=8C=89?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2=EF=BC=8C?= =?UTF-8?q?SSO=20=E4=B8=8B=E9=9A=90=E5=8E=BB=E5=8F=A3=E4=BB=A4=E6=A1=86?= =?UTF-8?q?=E5=B9=B6=E8=B7=B3=E7=BD=91=E5=85=B3=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api.ts 新增 AuthMode 类型与 /api/admin/mode 调用 - AdminPage:先探测模式;sso 下 401 统一跳 /_auth/login?next=...,退出跳 /_auth/logout - 探测失败按 builtin 处理,默认行为不变 --- web/src/admin/AdminPage.tsx | 58 ++++++++++++++++++++++++++++++++----- web/src/api.ts | 3 ++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/web/src/admin/AdminPage.tsx b/web/src/admin/AdminPage.tsx index 0c3254f..cd70ee9 100644 --- a/web/src/admin/AdminPage.tsx +++ b/web/src/admin/AdminPage.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useState, type FormEvent } from 'react' -import { adminApi, ApiError, type AccountView, type ConnInfo } from '../api' +import { useCallback, useEffect, useRef, useState, type FormEvent } from 'react' +import { adminApi, ApiError, type AccountView, type AuthMode, type ConnInfo } from '../api' import { formatSize } from '../format' import { IconCopy, @@ -36,6 +36,12 @@ function connText(conn: ConnInfo): string { return `地址:${conn.url}\n用户名:${conn.user}\n口令:${conn.pass}` } +// SSO 模式下 401 统一交给网关登录页处理。 +function redirectToGatewayLogin() { + const next = window.location.pathname + window.location.search + window.location.href = `/_auth/login?next=${encodeURIComponent(next)}` +} + function Switch({ checked, onChange, @@ -98,6 +104,7 @@ function ConnModal({ conn, onClose }: { conn: ConnInfo; onClose: () => void }) { } export function AdminPage() { + const [mode, setMode] = useState(null) const [ready, setReady] = useState(false) const [authed, setAuthed] = useState(false) const [password, setPassword] = useState('') @@ -108,6 +115,15 @@ export function AdminPage() { const [showNew, setShowNew] = useState(false) const [newUser, setNewUser] = useState('') const [newReadonly, setNewReadonly] = useState(false) + const modeRef = useRef('builtin') + + const handleUnauthorized = useCallback(() => { + if (modeRef.current === 'sso') { + redirectToGatewayLogin() + return + } + setAuthed(false) + }, []) const load = useCallback(async () => { try { @@ -116,17 +132,33 @@ export function AdminPage() { setAuthed(true) } catch (err) { if (err instanceof ApiError && err.status === 401) { - setAuthed(false) + handleUnauthorized() } else { setError(err instanceof Error ? err.message : '加载失败') } } finally { setReady(true) } - }, []) + }, [handleUnauthorized]) useEffect(() => { - void load() + let alive = true + void (async () => { + let detected: AuthMode = 'builtin' + try { + const info = await adminApi.mode() + if (info.mode === 'sso') detected = 'sso' + } catch { + /* 取不到模式时按 builtin 处理,保持默认行为 */ + } + if (!alive) return + modeRef.current = detected + setMode(detected) + await load() + })() + return () => { + alive = false + } }, [load]) const run = async (fn: () => Promise) => { @@ -136,7 +168,7 @@ export function AdminPage() { await fn() } catch (err) { if (err instanceof ApiError && err.status === 401) { - setAuthed(false) + handleUnauthorized() } else { setError(err instanceof Error ? err.message : '操作失败') } @@ -155,6 +187,10 @@ export function AdminPage() { } const logout = () => { + if (mode === 'sso') { + window.location.href = '/_auth/logout' + return + } void run(async () => { await adminApi.logout() setAuthed(false) @@ -206,7 +242,7 @@ export function AdminPage() { }) } - if (!ready) { + if (!ready || mode === null) { return (
加载中
@@ -215,6 +251,14 @@ export function AdminPage() { } if (!authed) { + // SSO 模式不显示口令输入框;未认证时已跳转网关登录页。 + if (mode === 'sso') { + return ( +
+
正在跳转登录
+
+ ) + } return (
diff --git a/web/src/api.ts b/web/src/api.ts index f265013..aacfe03 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -14,6 +14,8 @@ export interface ConnInfo { pass: string } +export type AuthMode = 'builtin' | 'sso' + export interface Entry { name: string isDir: boolean @@ -58,6 +60,7 @@ async function request(method: string, url: string, body?: unknown): Promise< export { ApiError } export const adminApi = { + mode: () => request<{ mode: AuthMode }>('GET', '/api/admin/mode'), login: (password: string) => request<{ ok: boolean }>('POST', '/api/admin/login', { password }), logout: () => request<{ ok: boolean }>('POST', '/api/admin/logout'), list: () => request('GET', '/api/admin/accounts'),