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:
19
frontend/src/app/(auth)/layout.tsx
Normal file
19
frontend/src/app/(auth)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
frontend/src/app/(auth)/login/page.tsx
Normal file
23
frontend/src/app/(auth)/login/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
67
frontend/src/app/(auth)/password-reset/confirm/page.tsx
Normal file
67
frontend/src/app/(auth)/password-reset/confirm/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
frontend/src/app/(auth)/password-reset/page.tsx
Normal file
25
frontend/src/app/(auth)/password-reset/page.tsx
Normal 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'll send you an email with instructions to reset your password
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PasswordResetRequestForm showLoginLink />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
frontend/src/app/(auth)/register/page.tsx
Normal file
20
frontend/src/app/(auth)/register/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
30
frontend/src/app/providers.tsx
Normal file
30
frontend/src/app/providers.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user