feat(ST-l9kkxh2m): /reset-password pagina + resetPasswordAction + hashPassword
- lib/auth.ts: hashPassword(password) geëxporteerd (bcrypt, rounds=12) - actions/auth.ts: resetPasswordAction met Zod-validatie (min 8, superRefine gelijkheid), prisma.user.update (password_hash + must_reset_password=false), redirect /dashboard - app/(auth)/reset-password/page.tsx: server guard (userId check + must_reset_password check) - app/(auth)/reset-password/reset-form.tsx: client form (nieuw wachtwoord + bevestiging) - __tests__/actions/auth.test.ts: 3 tests voor resetPasswordAction
This commit is contained in:
parent
71281038ff
commit
a19ae89e37
5 changed files with 206 additions and 3 deletions
37
app/(auth)/reset-password/page.tsx
Normal file
37
app/(auth)/reset-password/page.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { redirect } from 'next/navigation'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { ResetPasswordForm } from './reset-form'
|
||||
|
||||
export default async function ResetPasswordPage() {
|
||||
const session = await getSession()
|
||||
if (!session.userId) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.userId },
|
||||
select: { must_reset_password: true },
|
||||
})
|
||||
|
||||
if (!user?.must_reset_password) {
|
||||
redirect('/dashboard')
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm space-y-6">
|
||||
<div className="text-center space-y-1">
|
||||
<h1 className="text-2xl font-medium text-foreground">Wachtwoord wijzigen</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Kies een nieuw wachtwoord om verder te gaan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-container-low rounded-xl p-6 space-y-4 border border-border">
|
||||
<ResetPasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
74
app/(auth)/reset-password/reset-form.tsx
Normal file
74
app/(auth)/reset-password/reset-form.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
'use client'
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { useFormStatus } from 'react-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { resetPasswordAction } from '@/actions/auth'
|
||||
|
||||
type ActionResult = { error: string | Record<string, string[]> } | undefined
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<Button type="submit" className="w-full" disabled={pending}>
|
||||
{pending ? 'Even wachten…' : 'Wachtwoord opslaan'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function getErrorMessage(error: ActionResult): string | null {
|
||||
if (!error) return null
|
||||
if (typeof error.error === 'string') return error.error
|
||||
const first = Object.values(error.error).flat()[0]
|
||||
return first ?? null
|
||||
}
|
||||
|
||||
export function ResetPasswordForm() {
|
||||
const [state, formAction] = useActionState(resetPasswordAction, undefined)
|
||||
const errorMessage = getErrorMessage(state)
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="password" className="text-sm font-medium text-foreground">
|
||||
Nieuw wachtwoord
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={8}
|
||||
placeholder="••••••••"
|
||||
className="bg-input-background border-border focus-visible:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="confirm" className="text-sm font-medium text-foreground">
|
||||
Bevestig wachtwoord
|
||||
</label>
|
||||
<Input
|
||||
id="confirm"
|
||||
name="confirm"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={8}
|
||||
placeholder="••••••••"
|
||||
className="bg-input-background border-border focus-visible:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<div className="bg-error-container text-error-container-foreground rounded-lg px-3 py-2 text-sm border-l-4 border-error">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SubmitButton />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue