Add password reset functionality with form components, pages, and tests

- Implemented `PasswordResetRequestForm` and `PasswordResetConfirmForm` components with email and password validation, strength indicators, and error handling.
- Added dedicated pages for requesting and confirming password resets, integrated with React Query hooks and Next.js API routes.
- Included tests for validation rules, UI states, and token handling to ensure proper functionality and coverage.
- Updated ESLint and configuration files for new components and pages.
- Enhanced `IMPLEMENTATION_PLAN.md` with updated task details and documentation for password reset workflows.
This commit is contained in:
Felipe Cardoso
2025-11-01 00:57:57 +01:00
parent dbb05289b2
commit 925950d58e
25 changed files with 2306 additions and 74 deletions

View File

@@ -0,0 +1,19 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Authentication',
};
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900 px-4 py-12 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,23 @@
'use client';
import { LoginForm } from '@/components/auth/LoginForm';
export default function LoginPage() {
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">
Sign in to your account
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Access your dashboard and manage your account
</p>
</div>
<LoginForm
showRegisterLink
showPasswordResetLink
/>
</div>
);
}

View File

@@ -0,0 +1,67 @@
/**
* Password Reset Confirm Page
* Users set a new password using the token from their email
*/
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
import { PasswordResetConfirmForm } from '@/components/auth/PasswordResetConfirmForm';
import { Alert } from '@/components/ui/alert';
import Link from 'next/link';
export default function PasswordResetConfirmPage() {
const searchParams = useSearchParams();
const router = useRouter();
const token = searchParams.get('token');
// Handle successful password reset
const handleSuccess = () => {
// Wait 3 seconds then redirect to login
setTimeout(() => {
router.push('/login');
}, 3000);
};
// If no token in URL, show error
if (!token) {
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">
Invalid Reset Link
</h2>
</div>
<Alert variant="destructive">
<p className="text-sm">
This password reset link is invalid or has expired. Please request a new
password reset.
</p>
</Alert>
<div className="text-center">
<Link
href="/password-reset"
className="text-sm text-primary underline-offset-4 hover:underline font-medium"
>
Request new reset link
</Link>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">Set new password</h2>
<p className="mt-2 text-sm text-muted-foreground">
Choose a strong password for your account
</p>
</div>
<PasswordResetConfirmForm token={token} onSuccess={handleSuccess} showLoginLink />
</div>
);
}

View File

@@ -0,0 +1,25 @@
/**
* Password Reset Request Page
* Users enter their email to receive reset instructions
*/
'use client';
import { PasswordResetRequestForm } from '@/components/auth/PasswordResetRequestForm';
export default function PasswordResetPage() {
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">
Reset your password
</h2>
<p className="mt-2 text-sm text-muted-foreground">
We&apos;ll send you an email with instructions to reset your password
</p>
</div>
<PasswordResetRequestForm showLoginLink />
</div>
);
}

View File

@@ -0,0 +1,20 @@
'use client';
import { RegisterForm } from '@/components/auth/RegisterForm';
export default function RegisterPage() {
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">
Create your account
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Get started with your free account today
</p>
</div>
<RegisterForm showLoginLink />
</div>
);
}

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "FastNext Template",
description: "FastAPI + Next.js Template",
};
export default function RootLayout({
@@ -27,7 +28,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Providers>{children}</Providers>
</body>
</html>
);

View File

@@ -0,0 +1,30 @@
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
retry: 1,
refetchOnWindowFocus: true,
},
mutations: {
retry: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}