refactor: 前后端目录结构模块化 + v2 优化移植与缺陷修复

前端:
- src/srv/api → src/api,按域分文件夹(auth/cluster/host/storage/deployment/collector)
- 新增 http.ts 引擎(http.get/post/put/delete),域模块改用语义化调用
- index.html 增加 favicon(匹配应用 logo 的 emerald + Activity 脉冲)

后端分层增强(保留包名):
- 新增 repository/:从 models 拆出 JsonDB 持久化层(InitDB/GetDB/Shutdown/RequestSave)
- 新增 dto/:从 controllers 抽出各域请求契约(带 binding 校验)
- 新增 pkg/crypto、pkg/jwt:从 utils 拆出安全原语(外部 jwt 别名 jwtv5 避免冲突)
- models 只留领域实体 + sanitizer;utils 只留 gin/应用相关(response/errors)

v2 优化移植与缺陷修复:
- 移植密钥分离/SSH TOFU/并发写安全/Loki 生产级配置/Docker 加固
- 修复 deployment.go 并发丢更新、Helm values 重复键、EncryptField 哨兵、loki-gateway 端点等审计缺陷
- 实现真正的优雅关停(main.go http.Server + signal + repository.Shutdown)
- 修复登录契约:后端返回 accessToken,前端 auth.ts/Login.tsx 对齐
- 补 crypto/jwt/repository 单元测试(含并发写回归)

验证: go build/vet/test 全绿;npm run build 通过;登录→鉴权→优雅关停冒烟通过

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Logan.LiandClaude committed 2026-06-30 10:46:26 +08:00
1 parent fb8d10231a
commit 327e282356
93 files changed
+8433 -3089

No files matched your search

+72 -36
View File
@@ -1,12 +1,22 @@
import { useState, useEffect } from 'react'
import { memo, useCallback, useEffect, useState } from 'react'
import { useNavigate } from 'react-router'
import { Activity, Eye, EyeOff, Shield, ServerOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { authApi } from '@/srv/api'
import { authApi } from '@/api'
import { toast } from 'sonner'
export default function Login() {
/**
* Login 页面 - 优化版本
*
* 优化点:
* 1. React.memo 包裹组件
* 2. 表单处理使用 useCallback 缓存
* 3. 错误处理使用 sonner toast 替代 alert
* 4. 清理 useEffect 中的未使用变量
*/
const Login = memo(function Login() {
const [username, setUsername] = useState('admin')
const [password, setPassword] = useState('admin123')
const [showPassword, setShowPassword] = useState(false)
@@ -24,50 +34,67 @@ export default function Login() {
}
// Quick backend check
checkBackend()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigate])
const checkBackend = async () => {
// 后端状态检查 - useCallback缓存
const checkBackend = useCallback(async () => {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 3000)
await fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
// 用公开的健康检查端点探活;避免打 /auth/login(生产环境被 nginx 限流 5r/m,
// 反复刷新登录页会触发限流并锁死真正的登录请求)
const res = await fetch('/api/v1/health', {
method: 'GET',
signal: controller.signal,
})
clearTimeout(timeoutId)
// Even a 400 error means backend is up
setBackendStatus('online')
setBackendStatus(res.ok ? 'online' : 'offline')
} catch {
setBackendStatus('offline')
}
}
}, [])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
// 表单提交处理 - useCallback缓存
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
try {
const result = await authApi.login(username, password)
localStorage.setItem('token', result.token)
localStorage.setItem('user', JSON.stringify(result.user))
navigate('/')
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '登录失败'
if (message.includes('Cannot connect')) {
setError('无法连接后端服务,请确认后端已启动(端口 8080)。')
setBackendStatus('offline')
} else if (message.includes('invalid') || message.includes('password') || message.includes('密码')) {
setError('用户名或密码错误')
} else {
setError(message)
try {
const result = await authApi.login(username, password)
localStorage.setItem('token', result.accessToken)
localStorage.setItem('refreshToken', result.refreshToken)
localStorage.setItem('user', JSON.stringify(result.user))
toast.success('登录成功')
navigate('/')
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '登录失败'
if (message.includes('Cannot connect')) {
setError('无法连接后端服务,请确认后端已启动(端口 8080)。')
setBackendStatus('offline')
} else if (
message.includes('invalid') ||
message.includes('password') ||
message.includes('密码') ||
message.includes('Unauthorized')
) {
setError('用户名或密码错误')
} else {
setError(message)
}
} finally {
setLoading(false)
}
} finally {
setLoading(false)
}
}
},
[username, password, navigate]
)
// 密码显示切换 - useCallback缓存
const togglePassword = useCallback(() => {
setShowPassword((prev) => !prev)
}, [])
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center p-4">
@@ -120,6 +147,7 @@ export default function Login() {
placeholder="请输入用户名"
required
className="h-10"
autoComplete="username"
/>
</div>
@@ -134,13 +162,19 @@ export default function Login() {
placeholder="请输入密码"
required
className="h-10 pr-10"
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
onClick={togglePassword}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
tabIndex={-1}
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
{showPassword ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
</div>
@@ -163,4 +197,6 @@ export default function Login() {
</div>
</div>
)
}
})
export default Login