chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

214 lines
6.6 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.

'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import DepartmentTreeSelect from '@/components/DepartmentTreeSelect'
import BrandLogo from '@/components/BrandLogo'
import { DEFAULT_LOGIN_LOGO_URL } from '@/lib/brand-assets'
import { parseFetchError, parseApiError } from '@/lib/error-handler'
export default function RegisterPage() {
const router = useRouter()
const [email, setEmail] = useState('')
const [username, setUsername] = useState('')
const [department, setDepartment] = useState('')
const [departments, setDepartments] = useState<Array<{ id: string; name: string; parentId?: string | null }>>([])
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
fetchDepartments()
}, [])
const fetchDepartments = async () => {
try {
const response = await fetch('/api/departments')
if (response.ok) {
const data = await response.json()
setDepartments(data)
}
} catch (error) {
console.error('Error fetching departments:', error)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
if (password !== confirmPassword) {
setError('两次输入的密码不一致')
return
}
if (password.length < 6) {
setError('密码长度至少为6个字符')
return
}
setLoading(true)
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, username, department, password }),
})
const data = await response.json()
if (response.ok) {
router.push('/login?registered=true')
} else {
// 直接使用已解析的data,不要再次调用parseFetchError
setError(data.error || '注册失败,请稍后再试')
}
} catch (error) {
console.error('Registration error:', error)
setError('注册失败,请稍后再试')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100 p-4">
<div className="max-w-md w-full">
{/* Logo */}
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
<BrandLogo
src={DEFAULT_LOGIN_LOGO_URL}
fallbackSrc={DEFAULT_LOGIN_LOGO_URL}
alt=""
width={80}
height={80}
unoptimized
className="rounded-apple shadow-apple"
/>
</div>
<h1 className="text-3xl font-bold text-gray-900">由壹点3D素材</h1>
<p className="text-gray-600 mt-2">创建您的账号,开始分享3D素材</p>
</div>
{/* Register Form */}
<div className="card-apple p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-apple text-sm">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
邮箱
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="input-apple"
placeholder="your@email.com"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
用户名
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="input-apple"
placeholder="选择一个用户名"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
部门
</label>
<DepartmentTreeSelect
departments={departments}
value={department}
onChange={(deptId) => {
// 将部门ID转换为部门名称(API接受名称)
const dept = departments.find(d => d.id === deptId || d.name === deptId)
setDepartment(dept ? dept.name : '')
}}
placeholder="请选择部门"
allowEmpty={true}
emptyLabel="未分配"
className="w-full"
/>
{departments.length === 0 && (
<p className="text-sm text-gray-500 mt-1">
暂无可选部门,请联系管理员添加
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
密码
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input-apple"
placeholder="••••••••"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
确认密码
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="input-apple"
placeholder="••••••••"
required
/>
</div>
<button
type="submit"
className="w-full btn-primary"
disabled={loading}
>
{loading ? '注册中...' : '注册'}
</button>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
已有账号?{' '}
<Link
href="/login"
className="text-gray-900 hover:text-gray-700 font-medium underline"
>
立即登录
</Link>
</p>
</div>
</div>
</div>
</div>
)
}