feat(auth): 增强注册流程并完善默认邮箱初始化
- 注册页支持选择可用域名并自动创建邮箱,密码输入改为可见性切换组件。 - 后端默认管理员不再依赖固定密码,未配置时自动生成随机密码并创建对应域名、邮箱和欢迎消息。 - 公共配置接口返回可注册域名,补充 404 页面与相关路由。 - 更新测试以适配新的默认种子数据和邮箱创建逻辑。
This commit is contained in:
@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { api } from "@/lib/api"
|
||||
import { useMe } from "@/hooks/use-me"
|
||||
import { TurnstileBox } from "@/components/turnstile-box"
|
||||
import { PasswordInput } from "@/components/ui/password-input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
@@ -47,7 +48,7 @@ export function LoginPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Input id="password" name="password" type="password" autoComplete="current-password" required className="h-11 text-base" />
|
||||
<PasswordInput id="password" name="password" autoComplete="current-password" required className="h-11 text-base" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Home, MailQuestion } from "lucide-react"
|
||||
|
||||
export function NotFoundPage() {
|
||||
return (
|
||||
<div className="grid min-h-screen place-items-center bg-background px-4">
|
||||
<div className="w-full max-w-sm text-center">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-muted">
|
||||
<MailQuestion className="h-10 w-10 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mb-2 text-6xl font-bold tracking-tight">404</h1>
|
||||
<p className="mb-8 text-lg text-muted-foreground">页面不存在</p>
|
||||
<div className="flex justify-center gap-3">
|
||||
<Button asChild>
|
||||
<Link to="/">
|
||||
<Home className="mr-2 h-4 w-4" />返回首页
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/login">去登录</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { useMe } from "@/hooks/use-me"
|
||||
import { useLogout } from "@/hooks/use-logout"
|
||||
import { validatePasswordConfirm } from "@/lib/validation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { PasswordInput } from "@/components/ui/password-input"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -351,14 +352,14 @@ function ProfileOverview({ user, profile, password, passwordFormRef, stats, disp
|
||||
<CardContent>
|
||||
<form ref={passwordFormRef} className="space-y-4" onSubmit={(e) => { e.preventDefault(); password.mutate(new FormData(e.currentTarget)) }}>
|
||||
<Field label="当前密码">
|
||||
<Input name="currentPassword" type="password" required />
|
||||
<PasswordInput name="currentPassword" required />
|
||||
</Field>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Field label="新密码">
|
||||
<Input name="newPassword" type="password" minLength={8} required />
|
||||
<PasswordInput name="newPassword" minLength={8} required />
|
||||
</Field>
|
||||
<Field label="确认新密码">
|
||||
<Input name="confirmPassword" type="password" minLength={8} required />
|
||||
<PasswordInput name="confirmPassword" minLength={8} required />
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
|
||||
@@ -2,11 +2,14 @@ import * as React from "react"
|
||||
import { Link, Navigate, useNavigate } from "react-router-dom"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { api } from "@/lib/api"
|
||||
import type { PublicDomain } from "@/lib/api"
|
||||
import { useMe } from "@/hooks/use-me"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { PasswordInput } from "@/components/ui/password-input"
|
||||
import { TurnstileBox } from "@/components/turnstile-box"
|
||||
import { validatePasswordConfirm } from "@/lib/validation"
|
||||
|
||||
@@ -17,11 +20,30 @@ export function RegisterPage() {
|
||||
const { toast } = useToast()
|
||||
const publicSettings = useQuery({ queryKey: ["public-settings"], queryFn: api.publicSettings })
|
||||
const [turnstileToken, setTurnstileToken] = React.useState("")
|
||||
const [domainId, setDomainId] = React.useState("")
|
||||
const domains: PublicDomain[] = publicSettings.data?.mailboxDomains || []
|
||||
const selectedDomain = domains.find((d) => d.id === domainId)
|
||||
|
||||
const register = useMutation({
|
||||
mutationFn: (form: FormData) => {
|
||||
const password = String(form.get("password") || "")
|
||||
const confirmPassword = String(form.get("confirmPassword") || "")
|
||||
validatePasswordConfirm(password, confirmPassword)
|
||||
|
||||
if (domainId && selectedDomain) {
|
||||
const localPart = String(form.get("localPart") || "").trim()
|
||||
if (!localPart) throw new Error("请输入邮箱前缀")
|
||||
return api.register({
|
||||
email: `${localPart}@${selectedDomain.name}`,
|
||||
displayName: String(form.get("displayName") || ""),
|
||||
password,
|
||||
turnstileToken,
|
||||
domainId,
|
||||
localPart,
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback: no domains available, use email directly
|
||||
return api.register({
|
||||
email: String(form.get("email") || ""),
|
||||
displayName: String(form.get("displayName") || ""),
|
||||
@@ -53,21 +75,41 @@ export function RegisterPage() {
|
||||
</div>
|
||||
) : (
|
||||
<form className="space-y-5" onSubmit={(e) => { e.preventDefault(); if (turnstileRequired && !turnstileToken) { toast({ title: "请先完成人机验证" }); return }; register.mutate(new FormData(e.currentTarget)) }}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input id="email" name="email" type="email" autoComplete="username" required className="h-11 text-base" />
|
||||
</div>
|
||||
{domains.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="localPart">邮箱地址</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input id="localPart" name="localPart" className="h-11 flex-1 text-base" placeholder="your-name" required />
|
||||
<span className="text-sm text-muted-foreground">@</span>
|
||||
<Select value={domainId} onValueChange={setDomainId} required>
|
||||
<SelectTrigger className="h-11 w-[140px]">
|
||||
<SelectValue placeholder="选择域名" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{domains.map((d) => (
|
||||
<SelectItem key={d.id} value={d.id}>{d.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input id="email" name="email" type="email" autoComplete="username" required className="h-11 text-base" />
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="displayName">显示名称</Label>
|
||||
<Input id="displayName" name="displayName" autoComplete="name" className="h-11 text-base" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Input id="password" name="password" type="password" autoComplete="new-password" minLength={8} required className="h-11 text-base" />
|
||||
<PasswordInput id="password" name="password" autoComplete="new-password" minLength={8} required className="h-11 text-base" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">确认密码</Label>
|
||||
<Input id="confirmPassword" name="confirmPassword" type="password" autoComplete="new-password" minLength={8} required className="h-11 text-base" />
|
||||
<PasswordInput id="confirmPassword" name="confirmPassword" autoComplete="new-password" minLength={8} required className="h-11 text-base" />
|
||||
</div>
|
||||
{turnstileRequired && <TurnstileBox siteKey={publicSettings.data?.turnstileSiteKey || ""} onToken={setTurnstileToken} />}
|
||||
<Button className="h-11 w-full text-base" disabled={register.isPending || publicSettings.isLoading}>
|
||||
|
||||
Reference in New Issue
Block a user