feat(web): admin 页按认证模式切换,SSO 下隐去口令框并跳网关登录

- api.ts 新增 AuthMode 类型与 /api/admin/mode 调用
- AdminPage:先探测模式;sso 下 401 统一跳 /_auth/login?next=...,退出跳 /_auth/logout
- 探测失败按 builtin 处理,默认行为不变
This commit is contained in:
Yunling Zhang committed 2026-09-29 13:44:32 +08:00
1 parent b3c7f04c2b
commit 06fe172bea
2 files changed
+54 -7

No files matched your search

+51 -7
View File
@@ -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<AuthMode | null>(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<AuthMode>('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<void>) => {
@@ -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 (
<div className="page">
<div className="center-note">加载中</div>
@@ -215,6 +251,14 @@ export function AdminPage() {
}
if (!authed) {
// SSO 模式不显示口令输入框;未认证时已跳转网关登录页。
if (mode === 'sso') {
return (
<div className="page">
<div className="center-note">正在跳转登录</div>
</div>
)
}
return (
<div className="page narrow">
<header className="masthead">
+3
View File
@@ -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<T>(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<AccountView[]>('GET', '/api/admin/accounts'),