feat: custom password reset flow (token-based, Appwrite Messaging)
- db: new password_resets table (email, userId, tokenHash, expiresAt, usedAt) - lib: password-reset-actions.ts — requestPasswordResetAction, verifyResetToken, resetPasswordAction - lib: Messaging added to createAdminClient - page: /reset-password — validates token server-side, shows form or error card - page: /forgot-password — now uses requestPasswordResetAction (custom flow) - page: /sign-in — shows success banner after ?reset=success redirect
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useActionState } from "react";
|
||||
import { ArrowLeft, CircleNotch, ShieldCheck } from "@/lib/icons";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resetPasswordAction } from "@/lib/appwrite/password-reset-actions";
|
||||
import { initialAuthState } from "@/lib/appwrite/auth-types";
|
||||
|
||||
interface Props extends React.ComponentProps<"div"> {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function ResetPasswordForm({ token, className, ...props }: Props) {
|
||||
const [state, formAction, isPending] = useActionState(resetPasswordAction, initialAuthState);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="bg-primary/10 text-primary mx-auto mb-2 flex size-12 items-center justify-center rounded-full">
|
||||
<ShieldCheck className="size-6" />
|
||||
</div>
|
||||
<CardTitle className="text-xl">Yeni şifre belirle</CardTitle>
|
||||
<CardDescription>
|
||||
Kod doğrulandı. Yeni şifrenizi girin.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action={formAction} className="flex flex-col gap-4">
|
||||
<input type="hidden" name="token" value={token} />
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="password">Yeni şifre</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="En az 8 karakter"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={8}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="confirmPassword">Şifre tekrar</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Şifreyi tekrar girin"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-destructive text-center text-sm" role="alert">
|
||||
{state.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<CircleNotch className="size-4 animate-spin" />
|
||||
Güncelleniyor...
|
||||
</>
|
||||
) : (
|
||||
"Şifreyi güncelle"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="text-muted-foreground hover:text-foreground flex items-center justify-center gap-1 text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
<ArrowLeft className="size-3.5" />
|
||||
Giriş sayfasına dön
|
||||
</Link>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user