Security audit (2026-03-31): 5 HIGH + 10 MEDIUM issues, all fixed. HIGH: - H1: JWT password_version mechanism (pwv in Claims, middleware verification, auto-increment on password change) - H2: Docker saas port bound to 127.0.0.1 - H3: TOTP encryption key decoupled from JWT secret (production bailout) - H4+H5: Tauri CSP hardened (removed unsafe-inline, restricted connect-src) MEDIUM: - M1: Persistent rate limiting (PostgreSQL rate_limit_events table) - M2: Account lockout (5 failures -> 15min lock) - M3: RFC 5322 email validation with regex - M4: Device registration typed struct with length limits - M5: Provider URL validation on create/update (SSRF prevention) - M6: Legacy TOTP secret migration (fixed nonce -> random nonce) - M7: Legacy frontend crypto migration (static salt -> random salt) - M8+M9: Admin frontend: removed JS token storage, HttpOnly cookie only - M10: Pipeline debug log sanitization (keys only, 100-char truncation) Also: fixed CLAUDE.md Section 12 (was corrupted), added title.rs middleware skeleton, fixed RegisterDeviceRequest visibility.
161 lines
6.1 KiB
TypeScript
161 lines
6.1 KiB
TypeScript
// ============================================================
|
|
// 登录页面
|
|
// ============================================================
|
|
|
|
import { useState } from 'react'
|
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
import { LoginForm, ProFormText } from '@ant-design/pro-components'
|
|
import { LockOutlined, UserOutlined, SafetyOutlined } from '@ant-design/icons'
|
|
import { message } from 'antd'
|
|
import { authService } from '@/services/auth'
|
|
import { useAuthStore } from '@/stores/authStore'
|
|
import type { LoginRequest } from '@/types'
|
|
|
|
export default function Login() {
|
|
const navigate = useNavigate()
|
|
const [searchParams] = useSearchParams()
|
|
const loginStore = useAuthStore((s) => s.login)
|
|
const [needTotp, setNeedTotp] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const handleSubmit = async (values: Record<string, string>) => {
|
|
setLoading(true)
|
|
try {
|
|
const data: LoginRequest = {
|
|
username: values.username?.trim() || '',
|
|
password: values.password || '',
|
|
totp_code: values.totp_code?.trim() || undefined,
|
|
}
|
|
|
|
const res = await authService.login(data)
|
|
loginStore(res.account)
|
|
|
|
message.success('登录成功')
|
|
const from = searchParams.get('from') || '/'
|
|
navigate(from, { replace: true })
|
|
} catch (err: unknown) {
|
|
const error = err as { message?: string; status?: number }
|
|
const msg = error.message || ''
|
|
if (msg.includes('TOTP') || msg.includes('totp') || msg.includes('2FA') || msg.includes('验证码') || error.status === 403) {
|
|
setNeedTotp(true)
|
|
message.warning(msg || '请输入两步验证码')
|
|
} else {
|
|
message.error(msg || '登录失败,请检查用户名和密码')
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen flex">
|
|
{/* Left Brand Panel — hidden on mobile */}
|
|
<div className="hidden md:flex flex-1 flex-col items-center justify-center relative overflow-hidden"
|
|
style={{ background: 'linear-gradient(135deg, #0c0a09 0%, #1c1917 40%, #292524 100%)' }}
|
|
>
|
|
{/* Decorative gradient orb */}
|
|
<div
|
|
className="absolute w-[400px] h-[400px] rounded-full opacity-20 blur-3xl"
|
|
style={{ background: 'linear-gradient(135deg, #863bff, #47bfff)', top: '20%', left: '10%' }}
|
|
/>
|
|
<div
|
|
className="absolute w-[300px] h-[300px] rounded-full opacity-10 blur-3xl"
|
|
style={{ background: 'linear-gradient(135deg, #47bfff, #863bff)', bottom: '10%', right: '15%' }}
|
|
/>
|
|
|
|
{/* Brand content */}
|
|
<div className="relative z-10 text-center px-8">
|
|
<div
|
|
className="inline-flex items-center justify-center w-16 h-16 rounded-2xl mb-6"
|
|
style={{ background: 'linear-gradient(135deg, #863bff, #47bfff)' }}
|
|
>
|
|
<span className="text-white text-2xl font-bold">Z</span>
|
|
</div>
|
|
<h1 className="text-4xl font-bold text-white mb-3 tracking-tight">ZCLAW</h1>
|
|
<p className="text-white/50 text-base mb-8">AI Agent 管理平台</p>
|
|
<div className="w-16 h-px mx-auto mb-8" style={{ background: 'linear-gradient(90deg, transparent, #863bff, #47bfff, transparent)' }} />
|
|
<p className="text-white/30 text-sm max-w-sm mx-auto leading-relaxed">
|
|
统一管理 AI 服务商、模型配置、API 密钥、用量监控与系统配置
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Login Form */}
|
|
<div className="flex-1 md:flex-none md:w-[480px] flex items-center justify-center p-8 bg-white dark:bg-neutral-950">
|
|
<div className="w-full max-w-[360px]">
|
|
{/* Mobile logo (visible only on mobile) */}
|
|
<div className="md:hidden flex items-center gap-3 mb-10">
|
|
<div
|
|
className="flex items-center justify-center w-10 h-10 rounded-xl"
|
|
style={{ background: 'linear-gradient(135deg, #863bff, #47bfff)' }}
|
|
>
|
|
<span className="text-white font-bold">Z</span>
|
|
</div>
|
|
<span className="text-xl font-bold text-neutral-900 dark:text-white">ZCLAW</span>
|
|
</div>
|
|
|
|
<h2 className="text-2xl font-semibold text-neutral-900 dark:text-white mb-1">
|
|
登录
|
|
</h2>
|
|
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-8">
|
|
输入您的账号信息以继续
|
|
</p>
|
|
|
|
<LoginForm
|
|
onFinish={handleSubmit}
|
|
submitter={{
|
|
searchConfig: { submitText: '登录' },
|
|
submitButtonProps: {
|
|
loading,
|
|
block: true,
|
|
style: {
|
|
height: 44,
|
|
borderRadius: 8,
|
|
fontWeight: 500,
|
|
fontSize: 15,
|
|
background: 'linear-gradient(135deg, #863bff, #47bfff)',
|
|
border: 'none',
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<ProFormText
|
|
name="username"
|
|
fieldProps={{
|
|
size: 'large',
|
|
prefix: <UserOutlined />,
|
|
autoComplete: 'username',
|
|
}}
|
|
placeholder="请输入用户名"
|
|
rules={[{ required: true, message: '请输入用户名' }]}
|
|
/>
|
|
<ProFormText.Password
|
|
name="password"
|
|
fieldProps={{
|
|
size: 'large',
|
|
prefix: <LockOutlined />,
|
|
autoComplete: 'current-password',
|
|
}}
|
|
placeholder="请输入密码"
|
|
rules={[{ required: true, message: '请输入密码' }]}
|
|
/>
|
|
{needTotp && (
|
|
<ProFormText
|
|
name="totp_code"
|
|
fieldProps={{
|
|
size: 'large',
|
|
prefix: <SafetyOutlined />,
|
|
maxLength: 6,
|
|
autoComplete: 'one-time-code',
|
|
}}
|
|
placeholder="请输入 6 位验证码"
|
|
rules={[{ required: true, message: '请输入验证码' }]}
|
|
/>
|
|
)}
|
|
</LoginForm>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|