diff --git a/public/assets/icons/settings-shield-icon.svg b/public/assets/icons/settings-shield-icon.svg
new file mode 100644
index 0000000..b5a32ad
--- /dev/null
+++ b/public/assets/icons/settings-shield-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index a66ca75..574b8be 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -35,6 +35,17 @@ export default function LoginPage() {
return;
}
+ // Check if 2FA is required (response is wrapped in { success, data })
+ if (validateData.data?.requiresTwoFactor) {
+ // Redirect to 2FA verification page with tempToken
+ const params = new URLSearchParams({
+ token: validateData.data.tempToken,
+ email: email,
+ });
+ router.push(`/login/verify-2fa?${params.toString()}`);
+ return;
+ }
+
// If validation passed, use NextAuth to create session
const result = await signIn('credentials', {
email,
diff --git a/src/app/(auth)/login/verify-2fa/page.tsx b/src/app/(auth)/login/verify-2fa/page.tsx
new file mode 100644
index 0000000..60dcf5b
--- /dev/null
+++ b/src/app/(auth)/login/verify-2fa/page.tsx
@@ -0,0 +1,266 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { signIn } from 'next-auth/react';
+import Link from 'next/link';
+import { twoFactorService } from '@/services';
+
+export default function VerifyTwoFactorPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [code, setCode] = useState(['', '', '', '', '', '']);
+ const [backupCode, setBackupCode] = useState('');
+ const [useBackup, setUseBackup] = useState(false);
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
+
+ // Get tempToken and email from URL params
+ const tempToken = searchParams.get('token') || '';
+ const userEmail = searchParams.get('email') || '';
+
+ useEffect(() => {
+ // If no token, redirect to login
+ if (!tempToken) {
+ router.push('/login');
+ }
+ }, [tempToken, router]);
+
+ const handleCodeChange = (index: number, value: string) => {
+ // Only allow single digit
+ if (value.length > 1) {
+ value = value.slice(-1);
+ }
+
+ // Only allow numbers
+ if (value && !/^\d$/.test(value)) {
+ return;
+ }
+
+ const newCode = [...code];
+ newCode[index] = value;
+ setCode(newCode);
+
+ // Auto-focus next input
+ if (value && index < 5) {
+ inputRefs.current[index + 1]?.focus();
+ }
+
+ // Auto-submit when all digits are entered
+ if (value && index === 5 && newCode.every((digit) => digit !== '')) {
+ handleSubmit(newCode.join(''));
+ }
+ };
+
+ const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
+ if (e.key === 'Backspace' && !code[index] && index > 0) {
+ inputRefs.current[index - 1]?.focus();
+ }
+ };
+
+ const handlePaste = (e: React.ClipboardEvent) => {
+ e.preventDefault();
+ const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6);
+ if (pastedData.length === 6) {
+ const newCode = pastedData.split('');
+ setCode(newCode);
+ inputRefs.current[5]?.focus();
+ // Auto-submit after paste
+ setTimeout(() => handleSubmit(pastedData), 100);
+ }
+ };
+
+ const handleSubmit = async (codeValue?: string) => {
+ const verificationCode = codeValue || code.join('');
+
+ if (!useBackup && verificationCode.length !== 6) {
+ setError('Please enter a 6-digit code');
+ return;
+ }
+
+ if (useBackup && !backupCode.trim()) {
+ setError('Please enter a backup code');
+ return;
+ }
+
+ setLoading(true);
+ setError('');
+
+ try {
+ let response;
+
+ if (useBackup) {
+ response = await twoFactorService.verifyBackupCode({
+ tempToken,
+ backupCode: backupCode.trim(),
+ });
+ } else {
+ response = await twoFactorService.verifyLogin({
+ tempToken,
+ token: verificationCode,
+ });
+ }
+
+ // Now sign in with NextAuth using the tokens we received
+ // We need to call the NextAuth credentials provider with the tokens
+ const result = await signIn('credentials', {
+ email: userEmail,
+ accessToken: response.data.accessToken,
+ refreshToken: response.data.refreshToken,
+ redirect: false,
+ });
+
+ if (result?.error) {
+ setError('Failed to complete login. Please try again.');
+ setLoading(false);
+ } else {
+ router.push('/');
+ router.refresh();
+ }
+ } catch (err) {
+ setLoading(false);
+ if (err instanceof Error) {
+ setError(err.message);
+ } else {
+ setError('Invalid code. Please try again.');
+ }
+ // Clear code on error
+ if (!useBackup) {
+ setCode(['', '', '', '', '', '']);
+ inputRefs.current[0]?.focus();
+ }
+ }
+ };
+
+ const handleFormSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ handleSubmit();
+ };
+
+ if (!tempToken) {
+ return null; // Will redirect in useEffect
+ }
+
+ return (
+ <>
+ {/* Heading */}
+
+
+
+
+
Two-Factor Authentication
+
+ {useBackup
+ ? 'Enter one of your backup codes to continue'
+ : 'Enter the 6-digit code from your authenticator app'}
+
+
+
+ {/* Form */}
+
+
+ {/* Back to Login */}
+
+
+ ← Back to Login
+
+
+
+ {/* Help text */}
+
+
+ {useBackup ? (
+ <>
+ Backup codes are single-use. After using one, it will be invalidated. If you've
+ used all your backup codes, you'll need to generate new ones from your account
+ settings.
+ >
+ ) : (
+ <>
+ Open your authenticator app (Google Authenticator, Authy, etc.) and enter the 6-digit
+ code shown for Re-Quest.
+ >
+ )}
+