Files
LokiStack/frontend/src/pages/Login.tsx
T
Logan.LiandClaude 327e282356 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>
2026-06-30 10:46:26 +08:00

203 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 '@/api'
import { toast } from 'sonner'
/**
* 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)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [backendStatus, setBackendStatus] = useState<'checking' | 'online' | 'offline'>('checking')
const navigate = useNavigate()
useEffect(() => {
// Check if already logged in
const token = localStorage.getItem('token')
if (token) {
navigate('/')
return
}
// Quick backend check
checkBackend()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigate])
// 后端状态检查 - useCallback缓存
const checkBackend = useCallback(async () => {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 3000)
// 用公开的健康检查端点探活;避免打 /auth/login(生产环境被 nginx 限流 5r/m,
// 反复刷新登录页会触发限流并锁死真正的登录请求)
const res = await fetch('/api/v1/health', {
method: 'GET',
signal: controller.signal,
})
clearTimeout(timeoutId)
setBackendStatus(res.ok ? 'online' : 'offline')
} catch {
setBackendStatus('offline')
}
}, [])
// 表单提交处理 - 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.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)
}
},
[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">
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-emerald-500 mb-4">
<Activity className="w-9 h-9 text-white" />
</div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">LokiDeploy</h1>
<p className="text-sm text-slate-500 mt-1">Kubernetes Loki 日志部署平台</p>
</div>
{/* Backend Status */}
{backendStatus === 'offline' && (
<div className="mb-4 p-4 rounded-xl bg-amber-50 border border-amber-200 flex items-start gap-3">
<ServerOff className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-800">后端服务未启动</p>
<p className="text-xs text-amber-700 mt-1">
无法连接 API 服务,请先启动后端:
</p>
<code className="block mt-2 px-3 py-2 bg-amber-100 rounded text-xs font-mono text-amber-900">
./run.sh
</code>
</div>
</div>
)}
{/* Login Card */}
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-800 p-8">
<div className="flex items-center gap-2 mb-6">
<Shield className="w-5 h-5 text-emerald-500" />
<h2 className="text-lg font-semibold text-slate-800 dark:text-white">登录</h2>
</div>
{error && (
<div className="mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">用户名</Label>
<Input
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名"
required
className="h-10"
autoComplete="username"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">密码</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
required
className="h-10 pr-10"
autoComplete="current-password"
/>
<button
type="button"
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" />
)}
</button>
</div>
</div>
<Button
type="submit"
className="w-full h-10 bg-emerald-600 hover:bg-emerald-700 text-white"
disabled={loading}
>
{loading ? '登录中...' : '登录'}
</Button>
</form>
<div className="mt-6 pt-4 border-t border-slate-100 dark:border-slate-800">
<p className="text-xs text-center text-slate-500">
默认账号:admin / admin123
</p>
</div>
</div>
</div>
</div>
)
})
export default Login