diff --git a/maternal-web/app/settings/page.tsx b/maternal-web/app/settings/page.tsx
index 171149d..fa035f2 100644
--- a/maternal-web/app/settings/page.tsx
+++ b/maternal-web/app/settings/page.tsx
@@ -7,6 +7,7 @@ import { useState, useEffect } from 'react';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { usersApi } from '@/lib/api/users';
+import { MFASettings } from '@/components/settings/MFASettings';
import { motion } from 'framer-motion';
export default function SettingsPage() {
@@ -217,6 +218,17 @@ export default function SettingsPage() {
+ {/* Security Settings - MFA */}
+
+
+
+
+
+
{/* Account Actions */}
(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [successMessage, setSuccessMessage] = useState(null);
+
+ // TOTP Setup Dialog
+ const [totpDialogOpen, setTotpDialogOpen] = useState(false);
+ const [totpSetupData, setTotpSetupData] = useState(null);
+ const [verificationCode, setVerificationCode] = useState('');
+ const [isVerifying, setIsVerifying] = useState(false);
+ const [copiedCodes, setCopiedCodes] = useState>(new Set());
+
+ // Email MFA Dialog
+ const [emailDialogOpen, setEmailDialogOpen] = useState(false);
+ const [isSettingUpEmail, setIsSettingUpEmail] = useState(false);
+
+ // Disable MFA Dialog
+ const [disableDialogOpen, setDisableDialogOpen] = useState(false);
+ const [isDisabling, setIsDisabling] = useState(false);
+
+ // Load MFA status on mount
+ useEffect(() => {
+ loadMFAStatus();
+ }, []);
+
+ const loadMFAStatus = async () => {
+ try {
+ setIsLoading(true);
+ const status = await mfaApi.getStatus();
+ setMFAStatus(status);
+ setError(null);
+ } catch (err: any) {
+ console.error('Failed to load MFA status:', err);
+ setError('Failed to load MFA status');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleSetupTOTP = async () => {
+ try {
+ setIsLoading(true);
+ const setupData = await mfaApi.setupTOTP();
+ setTotpSetupData(setupData);
+ setTotpDialogOpen(true);
+ setError(null);
+ } catch (err: any) {
+ console.error('Failed to setup TOTP:', err);
+ setError(err.response?.data?.message || 'Failed to setup authenticator app');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleVerifyTOTP = async () => {
+ if (!verificationCode || verificationCode.length !== 6) {
+ setError('Please enter a valid 6-digit code');
+ return;
+ }
+
+ try {
+ setIsVerifying(true);
+ setError(null);
+ await mfaApi.enableTOTP(verificationCode);
+ setSuccessMessage('Two-factor authentication enabled successfully!');
+ setTotpDialogOpen(false);
+ setVerificationCode('');
+ setTotpSetupData(null);
+ await loadMFAStatus();
+ } catch (err: any) {
+ console.error('Failed to verify TOTP:', err);
+ setError(err.response?.data?.message || 'Invalid verification code');
+ } finally {
+ setIsVerifying(false);
+ }
+ };
+
+ const handleSetupEmailMFA = async () => {
+ try {
+ setIsSettingUpEmail(true);
+ setError(null);
+ await mfaApi.setupEmailMFA();
+ setSuccessMessage('Email-based 2FA enabled successfully! Check your email for backup codes.');
+ setEmailDialogOpen(false);
+ await loadMFAStatus();
+ } catch (err: any) {
+ console.error('Failed to setup email MFA:', err);
+ setError(err.response?.data?.message || 'Failed to setup email 2FA');
+ } finally {
+ setIsSettingUpEmail(false);
+ }
+ };
+
+ const handleDisableMFA = async () => {
+ try {
+ setIsDisabling(true);
+ setError(null);
+ await mfaApi.disableMFA();
+ setSuccessMessage('Two-factor authentication disabled');
+ setDisableDialogOpen(false);
+ await loadMFAStatus();
+ } catch (err: any) {
+ console.error('Failed to disable MFA:', err);
+ setError(err.response?.data?.message || 'Failed to disable 2FA');
+ } finally {
+ setIsDisabling(false);
+ }
+ };
+
+ const copyBackupCode = (code: string, index: number) => {
+ navigator.clipboard.writeText(code);
+ setCopiedCodes(new Set(copiedCodes).add(index));
+ setTimeout(() => {
+ setCopiedCodes((prev) => {
+ const newSet = new Set(prev);
+ newSet.delete(index);
+ return newSet;
+ });
+ }, 2000);
+ };
+
+ if (isLoading && !mfaStatus) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Two-Factor Authentication
+
+ {mfaStatus?.enabled && (
+
+ )}
+
+
+
+ Add an extra layer of security to your account by enabling two-factor authentication.
+
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+ {successMessage && (
+ setSuccessMessage(null)}>
+ {successMessage}
+
+ )}
+
+ {!mfaStatus?.enabled ? (
+
+ }
+ onClick={handleSetupTOTP}
+ disabled={isLoading}
+ fullWidth
+ >
+ Setup Authenticator App (Recommended)
+
+ }
+ onClick={() => setEmailDialogOpen(true)}
+ disabled={isLoading}
+ fullWidth
+ >
+ Setup Email Authentication
+
+
+ ) : (
+
+
+ Two-factor authentication is currently enabled using{' '}
+ {mfaStatus.method === 'totp' ? 'Authenticator App' : 'Email'}.
+
+
+
+ )}
+
+
+
+ {/* TOTP Setup Dialog */}
+
+
+ {/* Email MFA Dialog */}
+
+
+ {/* Disable MFA Dialog */}
+
+ >
+ );
+}
diff --git a/maternal-web/lib/api/mfa.ts b/maternal-web/lib/api/mfa.ts
new file mode 100644
index 0000000..c825afd
--- /dev/null
+++ b/maternal-web/lib/api/mfa.ts
@@ -0,0 +1,93 @@
+import axios from 'axios';
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020';
+
+export interface MFAStatus {
+ enabled: boolean;
+ method?: 'totp' | 'email';
+ hasBackupCodes: boolean;
+}
+
+export interface TOTPSetupResult {
+ secret: string;
+ qrCodeUrl: string;
+ backupCodes: string[];
+}
+
+export const mfaApi = {
+ // Get MFA status
+ async getStatus(): Promise {
+ const response = await axios.get(`${API_BASE_URL}/api/v1/auth/mfa/status`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Setup TOTP (Google Authenticator)
+ async setupTOTP(): Promise {
+ const response = await axios.post(
+ `${API_BASE_URL}/api/v1/auth/mfa/totp/setup`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ }
+ );
+ return response.data;
+ },
+
+ // Enable TOTP
+ async enableTOTP(code: string): Promise<{ success: boolean; message: string }> {
+ const response = await axios.post(
+ `${API_BASE_URL}/api/v1/auth/mfa/totp/enable`,
+ { code },
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ }
+ );
+ return response.data;
+ },
+
+ // Setup Email MFA
+ async setupEmailMFA(): Promise<{ success: boolean; message: string }> {
+ const response = await axios.post(
+ `${API_BASE_URL}/api/v1/auth/mfa/email/setup`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ }
+ );
+ return response.data;
+ },
+
+ // Disable MFA
+ async disableMFA(): Promise<{ success: boolean; message: string }> {
+ const response = await axios.delete(`${API_BASE_URL}/api/v1/auth/mfa`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Regenerate backup codes
+ async regenerateBackupCodes(): Promise<{ success: boolean; backupCodes: string[] }> {
+ const response = await axios.post(
+ `${API_BASE_URL}/api/v1/auth/mfa/backup-codes/regenerate`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ }
+ );
+ return response.data;
+ },
+};