oa-ai/src/app/setup-password/page.tsx

247 lines
8.0 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 { Suspense, useState, useMemo } from 'react'
import { useSearchParams } from 'next/navigation'
interface PasswordStrength {
minLength: boolean
hasUpper: boolean
hasLower: boolean
hasDigit: boolean
hasSpecial: boolean
score: number
}
function checkPassword(pw: string): PasswordStrength {
const minLength = pw.length >= 8
const hasUpper = /[A-Z]/.test(pw)
const hasLower = /[a-z]/.test(pw)
const hasDigit = /[0-9]/.test(pw)
const hasSpecial = /[^A-Za-z0-9]/.test(pw)
const score = [hasUpper, hasLower, hasDigit, hasSpecial].filter(Boolean).length
return { minLength, hasUpper, hasLower, hasDigit, hasSpecial, score }
}
const checks: { key: keyof PasswordStrength; label: string }[] = [
{ key: 'minLength', label: '至少 8 位字符' },
{ key: 'hasUpper', label: '包含大写字母' },
{ key: 'hasLower', label: '包含小写字母' },
{ key: 'hasDigit', label: '包含数字' },
{ key: 'hasSpecial', label: '包含特殊字符' },
]
function SetupPasswordForm() {
const searchParams = useSearchParams()
const token = searchParams.get('token') || ''
const [password, setPassword] = useState('')
const [confirm, setConfirm] = useState('')
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
const [loading, setLoading] = useState(false)
const strength = useMemo(() => checkPassword(password), [password])
const isStrong = strength.minLength && strength.score >= 3
const passwordMismatch = confirm.length > 0 && password !== confirm
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError('')
if (!isStrong) {
setError('请先满足密码复杂度要求')
return
}
if (password !== confirm) {
setError('两次输入的密码不一致')
return
}
setLoading(true)
try {
const res = await fetch('/api/auth/setup-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, password }),
})
const data = await res.json()
if (!res.ok) {
setError(data.error || '设置失败')
} else {
setSuccess(true)
}
} catch {
setError('网络错误,请重试')
} finally {
setLoading(false)
}
}
if (!token) {
return (
<div style={styles.container}>
<div style={styles.card}>
<h1 style={styles.title}></h1>
<p style={styles.text}> token</p>
<a href="/login" style={styles.link}></a>
</div>
</div>
)
}
if (success) {
return (
<div style={styles.container}>
<div style={styles.card}>
<h1 style={{ ...styles.title, color: '#16a34a' }}></h1>
<p style={styles.text}>使</p>
<a href="/login" style={styles.link}></a>
</div>
</div>
)
}
return (
<div style={styles.container}>
<form onSubmit={handleSubmit} style={styles.card}>
<h1 style={styles.title}></h1>
<p style={styles.text}> OA </p>
{error && <div style={styles.error}>{error}</div>}
<label style={styles.label}>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={styles.input}
placeholder="至少 8 位,大写/小写/数字/特殊字符 4 选 3"
autoFocus
/>
{password.length > 0 && (
<div style={styles.complexityPanel}>
<p style={styles.complexityHint}> 8 + 3 </p>
{checks.map(c => {
const ok = c.key === 'minLength' ? strength.minLength : strength[c.key]
return (
<div key={c.key} style={styles.checkItem}>
<span style={{
display: 'inline-flex', width: 18, height: 18, borderRadius: '50%',
background: ok ? '#dcfce7' : '#f1f5f9',
color: ok ? '#16a34a' : '#94a3b8',
alignItems: 'center', justifyContent: 'center',
fontSize: 11, fontWeight: 700, flexShrink: 0,
}}>
{ok ? '✓' : '—'}
</span>
<span style={{ color: ok ? '#16a34a' : '#64748b' }}>{c.label}</span>
</div>
)
})}
<p style={{ fontSize: 12, marginTop: 6, color: isStrong ? '#16a34a' : '#d97706' }}>
{isStrong ? '密码强度符合要求' : `已满足 ${strength.score} 项(需至少 3 项)${strength.minLength ? '' : ',长度不足 8 位'}`}
</p>
</div>
)}
</label>
<label style={styles.label}>
<input
type="password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
style={{
...styles.input,
borderColor: passwordMismatch ? '#dc2626' : '#cbd5e1',
}}
placeholder="再次输入新密码"
/>
{passwordMismatch && (
<p style={{ color: '#dc2626', fontSize: 12, marginTop: 6 }}></p>
)}
</label>
<button
type="submit"
disabled={loading || !isStrong || passwordMismatch}
style={{
...styles.btn,
background: loading || !isStrong || passwordMismatch ? '#cbd5e1' : '#2563eb',
cursor: loading || !isStrong || passwordMismatch ? 'not-allowed' : 'pointer',
}}
>
{loading ? '设置中...' : passwordMismatch ? '请确认密码一致' : !isStrong ? '请满足密码复杂度要求' : '设置密码'}
</button>
</form>
</div>
)
}
export default function SetupPasswordPage() {
return (
<Suspense fallback={<div style={styles.loadingFallback}>...</div>}>
<SetupPasswordForm />
</Suspense>
)
}
const styles: Record<string, React.CSSProperties> = {
container: {
display: 'flex', justifyContent: 'center', alignItems: 'center',
minHeight: '100vh', padding: '20px',
background: '#f1f5f9',
},
loadingFallback: {
display: 'flex', justifyContent: 'center', alignItems: 'center',
minHeight: '100vh', color: '#64748b', fontSize: '14px',
},
card: {
background: '#fff', borderRadius: '12px',
padding: '40px', maxWidth: '420px', width: '100%',
boxShadow: '0 2px 12px rgba(0,0,0,0.08)',
},
title: {
fontSize: '22px', fontWeight: 700, margin: '0 0 12px',
color: '#0f172a',
},
text: {
fontSize: '14px', color: '#475569', margin: '0 0 24px', lineHeight: 1.6,
},
label: {
display: 'block', marginBottom: '16px',
fontSize: '13px', fontWeight: 500, color: '#334155',
},
input: {
display: 'block', width: '100%', marginTop: '6px',
padding: '10px 14px', borderRadius: '8px',
border: '1px solid #cbd5e1', background: '#fff',
color: '#0f172a', fontSize: '14px', boxSizing: 'border-box' as any,
outline: 'none',
},
complexityPanel: {
marginTop: 10, display: 'flex', flexDirection: 'column', gap: 4,
},
complexityHint: {
fontSize: 12, color: '#64748b', margin: '0 0 2px',
},
checkItem: {
display: 'flex', alignItems: 'center', gap: 6, fontSize: 12,
},
btn: {
width: '100%', padding: '12px', background: '#2563eb',
color: '#fff', border: 'none', borderRadius: '8px',
fontSize: '15px', fontWeight: 600,
marginTop: '8px',
},
error: {
background: '#fef2f2', color: '#dc2626', fontSize: '13px',
padding: '10px 14px', borderRadius: '8px', marginBottom: '16px',
},
link: {
display: 'inline-block', marginTop: '16px', color: '#2563eb',
textDecoration: 'none', fontSize: '14px', fontWeight: 500,
},
}