feat(auth): 添加异步密码哈希和验证函数
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

refactor(relay): 复用HTTP客户端和请求体序列化结果

feat(kernel): 添加获取单个审批记录的方法

fix(store): 改进SaaS连接错误分类和降级处理

docs: 更新审计文档和系统架构文档

refactor(prompt): 优化SQL查询参数化绑定

refactor(migration): 使用静态SQL和COALESCE更新配置项

feat(commands): 添加审批执行状态追踪和事件通知

chore: 更新启动脚本以支持Admin后台

fix(auth-guard): 优化授权状态管理和错误处理

refactor(db): 使用异步密码哈希函数

refactor(totp): 使用异步密码验证函数

style: 清理无用文件和注释

docs: 更新功能全景和审计文档

refactor(service): 优化HTTP客户端重用和请求处理

fix(connection): 改进SaaS不可用时的降级处理

refactor(handlers): 使用异步密码验证函数

chore: 更新依赖和工具链配置
This commit is contained in:
iven
2026-03-29 21:45:29 +08:00
parent b7ec317d2c
commit 7de294375b
34 changed files with 2041 additions and 894 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { useEffect, useState, useCallback, type ReactNode } from 'react'
import { useEffect, useState, useRef, useCallback, type ReactNode } from 'react'
import { useRouter } from 'next/navigation'
import { isAuthenticated, getAccount, clearAuth } from '@/lib/auth'
import { api, ApiRequestError } from '@/lib/api-client'
@@ -18,50 +18,61 @@ export function AuthGuard({ children }: AuthGuardProps) {
const [verifying, setVerifying] = useState(true)
const [connectionError, setConnectionError] = useState(false)
// Ref 跟踪授权状态,避免 useCallback 闭包捕获过时的 state
const authorizedRef = useRef(false)
// 防止并发验证RSC 导航可能触发多次 effect
const verifyingRef = useRef(false)
const verifyAuth = useCallback(async () => {
// 防止并发验证
if (verifyingRef.current) return
verifyingRef.current = true
setVerifying(true)
setConnectionError(false)
if (!isAuthenticated()) {
setVerifying(false)
verifyingRef.current = false
router.replace('/login')
return
}
// Already authorized? Skip re-verification on remount (e.g. Next.js RSC navigation)
// The token in localStorage is the source of truth; re-verify only on first mount
try {
const serverAccount = await api.auth.me()
setAccount(serverAccount)
setAuthorized(true)
authorizedRef.current = true
} catch (err) {
// Ignore abort errors — caused by navigation/SWR cancelling in-flight requests
// Keep current authorized state intact
// AbortError: 导航/SWR 取消了请求,忽略
// 如果已有授权ref 跟踪),保持不变;否则尝试 localStorage 缓存
if (err instanceof DOMException && err.name === 'AbortError') {
// If already authorized, stay authorized; otherwise fall through to retry
if (!authorized) {
// First mount was aborted — use cached account from localStorage
if (!authorizedRef.current) {
const cachedAccount = getAccount()
if (cachedAccount) {
setAccount(cachedAccount)
setAuthorized(true)
authorizedRef.current = true
}
}
return
}
// Only clear auth on actual authentication failures (401/403)
// Network errors, timeouts should NOT destroy the session
// 401/403: 真正的认证失败,清除 token
if (err instanceof ApiRequestError && (err.status === 401 || err.status === 403)) {
clearAuth()
authorizedRef.current = false
router.replace('/login')
} else {
// Transient error — show retry UI, keep token in localStorage
setConnectionError(true)
// 网络错误/超时 — 仅在未授权时显示连接错误
// 已授权的情况下忽略瞬态错误,保持当前状态
if (!authorizedRef.current) {
setConnectionError(true)
}
}
} finally {
setVerifying(false)
verifyingRef.current = false
}
}, [router, authorized])
}, [router])
useEffect(() => {
verifyAuth()