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:
egecankomur
2026-05-12 16:49:04 +03:00
parent 95e30a74c7
commit a3bcb464ea
8 changed files with 298 additions and 6 deletions
@@ -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>
);
}