forked from beilunyang/moemail
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement username/password authentication and registration fea…
…tures
- Loading branch information
1 parent
969d0ce
commit 126a4cb
Showing
24 changed files
with
2,643 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { register } from "@/lib/auth" | ||
import { authSchema } from "@/lib/validation" | ||
import { ZodError } from "zod" | ||
|
||
export const runtime = "edge" | ||
|
||
export async function POST(request: Request) { | ||
try { | ||
const json = await request.json() | ||
const body = authSchema.parse(json) | ||
|
||
await register(body.username, body.password) | ||
return Response.json({ success: true }) | ||
} catch (error) { | ||
if (error instanceof ZodError) { | ||
return Response.json( | ||
{ error: error.errors[0].message }, | ||
{ status: 400 } | ||
) | ||
} | ||
|
||
return Response.json( | ||
{ error: error instanceof Error ? error.message : "注册失败" }, | ||
{ status: 500 } | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
"use client" | ||
|
||
import { useState } from "react" | ||
import { signIn } from "next-auth/react" | ||
import { useToast } from "@/components/ui/use-toast" | ||
import { Button } from "@/components/ui/button" | ||
import { Input } from "@/components/ui/input" | ||
import { | ||
Card, | ||
CardContent, | ||
CardDescription, | ||
CardHeader, | ||
CardTitle, | ||
} from "@/components/ui/card" | ||
import { | ||
Tabs, | ||
TabsContent, | ||
TabsList, | ||
TabsTrigger, | ||
} from "@/components/ui/tabs" | ||
import { Github, Loader2, KeyRound, User2 } from "lucide-react" | ||
import { cn } from "@/lib/utils" | ||
|
||
interface FormErrors { | ||
username?: string | ||
password?: string | ||
confirmPassword?: string | ||
} | ||
|
||
export function LoginForm() { | ||
const [username, setUsername] = useState("") | ||
const [password, setPassword] = useState("") | ||
const [confirmPassword, setConfirmPassword] = useState("") | ||
const [loading, setLoading] = useState(false) | ||
const [errors, setErrors] = useState<FormErrors>({}) | ||
const { toast } = useToast() | ||
|
||
const validateLoginForm = () => { | ||
const newErrors: FormErrors = {} | ||
if (!username) newErrors.username = "请输入用户名" | ||
if (!password) newErrors.password = "请输入密码" | ||
if (password && password.length < 8) newErrors.password = "密码长度必须大于等于8位" | ||
setErrors(newErrors) | ||
return Object.keys(newErrors).length === 0 | ||
} | ||
|
||
const validateRegisterForm = () => { | ||
const newErrors: FormErrors = {} | ||
if (!username) newErrors.username = "请输入用户名" | ||
if (!password) newErrors.password = "请输入密码" | ||
if (password && password.length < 8) newErrors.password = "密码长度必须大于等于8位" | ||
if (!confirmPassword) newErrors.confirmPassword = "请确认密码" | ||
if (password !== confirmPassword) newErrors.confirmPassword = "两次输入的密码不一致" | ||
setErrors(newErrors) | ||
return Object.keys(newErrors).length === 0 | ||
} | ||
|
||
const handleLogin = async () => { | ||
if (!validateLoginForm()) return | ||
|
||
setLoading(true) | ||
try { | ||
const result = await signIn("credentials", { | ||
username, | ||
password, | ||
redirect: false, | ||
}) | ||
|
||
if (result?.error) { | ||
toast({ | ||
title: "登录失败", | ||
description: "用户名或密码错误", | ||
variant: "destructive", | ||
}) | ||
setLoading(false) | ||
return | ||
} | ||
|
||
window.location.href = "/" | ||
} catch (error) { | ||
toast({ | ||
title: "登录失败", | ||
description: error instanceof Error ? error.message : "请稍后重试", | ||
variant: "destructive", | ||
}) | ||
setLoading(false) | ||
} | ||
} | ||
|
||
const handleRegister = async () => { | ||
if (!validateRegisterForm()) return | ||
|
||
setLoading(true) | ||
try { | ||
const response = await fetch("/api/auth/register", { | ||
method: "POST", | ||
headers: { "Content-Type": "application/json" }, | ||
body: JSON.stringify({ username, password }), | ||
}) | ||
|
||
const data = await response.json() as { error?: string } | ||
|
||
if (!response.ok) { | ||
toast({ | ||
title: "注册失败", | ||
description: data.error || "请稍后重试", | ||
variant: "destructive", | ||
}) | ||
setLoading(false) | ||
return | ||
} | ||
|
||
// 注册成功后自动登录 | ||
const result = await signIn("credentials", { | ||
username, | ||
password, | ||
redirect: false, | ||
}) | ||
|
||
if (result?.error) { | ||
toast({ | ||
title: "登录失败", | ||
description: "自动登录失败,请手动登录", | ||
variant: "destructive", | ||
}) | ||
setLoading(false) | ||
return | ||
} | ||
|
||
window.location.href = "/" | ||
} catch (error) { | ||
toast({ | ||
title: "注册失败", | ||
description: error instanceof Error ? error.message : "请稍后重试", | ||
variant: "destructive", | ||
}) | ||
setLoading(false) | ||
} | ||
} | ||
|
||
const handleGithubLogin = () => { | ||
signIn("github", { callbackUrl: "/" }) | ||
} | ||
|
||
const clearForm = () => { | ||
setUsername("") | ||
setPassword("") | ||
setConfirmPassword("") | ||
setErrors({}) | ||
} | ||
|
||
return ( | ||
<Card className="w-[95%] max-w-lg border-2 border-primary/20"> | ||
<CardHeader className="space-y-2"> | ||
<CardTitle className="text-2xl text-center bg-gradient-to-r from-primary to-purple-600 bg-clip-text text-transparent"> | ||
欢迎使用 MoeMail | ||
</CardTitle> | ||
<CardDescription className="text-center"> | ||
萌萌哒临时邮箱服务 (。・∀・)ノ | ||
</CardDescription> | ||
</CardHeader> | ||
<CardContent className="px-6"> | ||
<Tabs defaultValue="login" className="w-full" onValueChange={clearForm}> | ||
<TabsList className="grid w-full grid-cols-2 mb-6"> | ||
<TabsTrigger value="login">登录</TabsTrigger> | ||
<TabsTrigger value="register">注册</TabsTrigger> | ||
</TabsList> | ||
<div className="min-h-[220px]"> | ||
<TabsContent value="login" className="space-y-4 mt-0"> | ||
<div className="space-y-3"> | ||
<div className="space-y-1.5"> | ||
<div className="relative"> | ||
<div className="absolute left-2.5 top-2 text-muted-foreground"> | ||
<User2 className="h-5 w-5" /> | ||
</div> | ||
<Input | ||
className={cn( | ||
"h-9 pl-9 pr-3", | ||
errors.username && "border-destructive focus-visible:ring-destructive" | ||
)} | ||
placeholder="用户名" | ||
value={username} | ||
onChange={(e) => { | ||
setUsername(e.target.value) | ||
setErrors({}) | ||
}} | ||
disabled={loading} | ||
/> | ||
</div> | ||
{errors.username && ( | ||
<p className="text-xs text-destructive">{errors.username}</p> | ||
)} | ||
</div> | ||
<div className="space-y-1.5"> | ||
<div className="relative"> | ||
<div className="absolute left-2.5 top-2 text-muted-foreground"> | ||
<KeyRound className="h-5 w-5" /> | ||
</div> | ||
<Input | ||
className={cn( | ||
"h-9 pl-9 pr-3", | ||
errors.password && "border-destructive focus-visible:ring-destructive" | ||
)} | ||
type="password" | ||
placeholder="密码" | ||
value={password} | ||
onChange={(e) => { | ||
setPassword(e.target.value) | ||
setErrors({}) | ||
}} | ||
disabled={loading} | ||
/> | ||
</div> | ||
{errors.password && ( | ||
<p className="text-xs text-destructive">{errors.password}</p> | ||
)} | ||
</div> | ||
</div> | ||
|
||
<div className="space-y-3 pt-1"> | ||
<Button | ||
className="w-full" | ||
onClick={handleLogin} | ||
disabled={loading} | ||
> | ||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} | ||
登录 | ||
</Button> | ||
|
||
<div className="relative"> | ||
<div className="absolute inset-0 flex items-center"> | ||
<span className="w-full border-t" /> | ||
</div> | ||
<div className="relative flex justify-center text-xs uppercase"> | ||
<span className="bg-background px-2 text-muted-foreground"> | ||
或者 | ||
</span> | ||
</div> | ||
</div> | ||
|
||
<Button | ||
variant="outline" | ||
className="w-full" | ||
onClick={handleGithubLogin} | ||
> | ||
<Github className="mr-2 h-4 w-4" /> | ||
使用 GitHub 账号登录 | ||
</Button> | ||
</div> | ||
</TabsContent> | ||
<TabsContent value="register" className="space-y-4 mt-0"> | ||
<div className="space-y-3"> | ||
<div className="space-y-1.5"> | ||
<div className="relative"> | ||
<div className="absolute left-2.5 top-2 text-muted-foreground"> | ||
<User2 className="h-5 w-5" /> | ||
</div> | ||
<Input | ||
className={cn( | ||
"h-9 pl-9 pr-3", | ||
errors.username && "border-destructive focus-visible:ring-destructive" | ||
)} | ||
placeholder="用户名" | ||
value={username} | ||
onChange={(e) => { | ||
setUsername(e.target.value) | ||
setErrors({}) | ||
}} | ||
disabled={loading} | ||
/> | ||
</div> | ||
{errors.username && ( | ||
<p className="text-xs text-destructive">{errors.username}</p> | ||
)} | ||
</div> | ||
<div className="space-y-1.5"> | ||
<div className="relative"> | ||
<div className="absolute left-2.5 top-2 text-muted-foreground"> | ||
<KeyRound className="h-5 w-5" /> | ||
</div> | ||
<Input | ||
className={cn( | ||
"h-9 pl-9 pr-3", | ||
errors.password && "border-destructive focus-visible:ring-destructive" | ||
)} | ||
type="password" | ||
placeholder="密码" | ||
value={password} | ||
onChange={(e) => { | ||
setPassword(e.target.value) | ||
setErrors({}) | ||
}} | ||
disabled={loading} | ||
/> | ||
</div> | ||
{errors.password && ( | ||
<p className="text-xs text-destructive">{errors.password}</p> | ||
)} | ||
</div> | ||
<div className="space-y-1.5"> | ||
<div className="relative"> | ||
<div className="absolute left-2.5 top-2 text-muted-foreground"> | ||
<KeyRound className="h-5 w-5" /> | ||
</div> | ||
<Input | ||
className={cn( | ||
"h-9 pl-9 pr-3", | ||
errors.confirmPassword && "border-destructive focus-visible:ring-destructive" | ||
)} | ||
type="password" | ||
placeholder="确认密码" | ||
value={confirmPassword} | ||
onChange={(e) => { | ||
setConfirmPassword(e.target.value) | ||
setErrors({}) | ||
}} | ||
disabled={loading} | ||
/> | ||
</div> | ||
{errors.confirmPassword && ( | ||
<p className="text-xs text-destructive">{errors.confirmPassword}</p> | ||
)} | ||
</div> | ||
</div> | ||
|
||
<div className="space-y-3 pt-1"> | ||
<Button | ||
className="w-full" | ||
onClick={handleRegister} | ||
disabled={loading} | ||
> | ||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} | ||
注册 | ||
</Button> | ||
</div> | ||
</TabsContent> | ||
</div> | ||
</Tabs> | ||
</CardContent> | ||
</Card> | ||
) | ||
} |
Oops, something went wrong.