feat: Implement user authentication for the admin web UI, including login, session management, and securing API calls.

This commit is contained in:
CJACK
2026-02-01 03:40:25 +08:00
parent 23bd4970d9
commit 4193336dd8
9 changed files with 437 additions and 34 deletions

View File

@@ -3,6 +3,7 @@ import AccountManager from './components/AccountManager'
import ApiTester from './components/ApiTester'
import BatchImport from './components/BatchImport'
import VercelSync from './components/VercelSync'
import Login from './components/Login'
const TABS = [
{ id: 'accounts', label: '🔑 账号管理' },
@@ -16,51 +17,154 @@ export default function App() {
const [config, setConfig] = useState({ keys: [], accounts: [] })
const [loading, setLoading] = useState(true)
const [message, setMessage] = useState(null)
const [token, setToken] = useState(null)
const [authChecking, setAuthChecking] = useState(true)
// 检查已存储的 Token
useEffect(() => {
const checkAuth = async () => {
// 检查 localStorage 或 sessionStorage
const storedToken = localStorage.getItem('ds2api_token') || sessionStorage.getItem('ds2api_token')
const expiresAt = parseInt(localStorage.getItem('ds2api_token_expires') || sessionStorage.getItem('ds2api_token_expires') || '0')
if (storedToken && expiresAt > Date.now()) {
// 验证 token 是否有效
try {
const res = await fetch('/admin/verify', {
headers: { 'Authorization': `Bearer ${storedToken}` }
})
if (res.ok) {
setToken(storedToken)
} else {
// Token 无效,清除
localStorage.removeItem('ds2api_token')
localStorage.removeItem('ds2api_token_expires')
sessionStorage.removeItem('ds2api_token')
sessionStorage.removeItem('ds2api_token_expires')
}
} catch {
// 网络错误,保留 token 重试
setToken(storedToken)
}
}
setAuthChecking(false)
}
checkAuth()
}, [])
// 带认证的 fetch
const authFetch = async (url, options = {}) => {
const headers = {
...options.headers,
'Authorization': `Bearer ${token}`
}
const res = await fetch(url, { ...options, headers })
// 401 时自动登出
if (res.status === 401) {
handleLogout()
throw new Error('认证已过期,请重新登录')
}
return res
}
const fetchConfig = async () => {
if (!token) return
try {
setLoading(true)
const res = await fetch('/admin/config')
const res = await authFetch('/admin/config')
if (res.ok) {
const data = await res.json()
setConfig(data)
}
} catch (e) {
console.error('获取配置失败:', e)
showMessage('error', e.message)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchConfig()
}, [])
if (token) {
fetchConfig()
}
}, [token])
const showMessage = (type, text) => {
setMessage({ type, text })
setTimeout(() => setMessage(null), 5000)
}
const handleLogin = (newToken) => {
setToken(newToken)
}
const handleLogout = () => {
setToken(null)
localStorage.removeItem('ds2api_token')
localStorage.removeItem('ds2api_token_expires')
sessionStorage.removeItem('ds2api_token')
sessionStorage.removeItem('ds2api_token_expires')
}
const renderTab = () => {
switch (activeTab) {
case 'accounts':
return <AccountManager config={config} onRefresh={fetchConfig} onMessage={showMessage} />
return <AccountManager config={config} onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} />
case 'test':
return <ApiTester config={config} onMessage={showMessage} />
return <ApiTester config={config} onMessage={showMessage} authFetch={authFetch} />
case 'import':
return <BatchImport onRefresh={fetchConfig} onMessage={showMessage} />
return <BatchImport onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} />
case 'vercel':
return <VercelSync onMessage={showMessage} />
return <VercelSync onMessage={showMessage} authFetch={authFetch} />
default:
return null
}
}
// 认证检查中
if (authChecking) {
return (
<div className="app">
<div className="login-container">
<div className="login-card">
<div className="empty-state">
<span className="loading"></span> 检查登录状态...
</div>
</div>
</div>
</div>
)
}
// 未登录
if (!token) {
return (
<div className="app">
{message && (
<div className={`alert alert-${message.type}`}>
{message.text}
</div>
)}
<Login onLogin={handleLogin} onMessage={showMessage} />
</div>
)
}
// 已登录
return (
<div className="app">
<header className="header">
<h1>DS2API Admin</h1>
<p>账号管理 · API 测试 · Vercel 部署</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1>DS2API Admin</h1>
<p>账号管理 · API 测试 · Vercel 部署</p>
</div>
<button className="btn btn-secondary btn-sm" onClick={handleLogout}>
🚪 登出
</button>
</div>
</header>
{message && (
@@ -104,3 +208,4 @@ export default function App() {
</div>
)
}