Frontend Compliance Features: - Created compliance API client (data export, account deletion, deletion status) - Added DataExport component with download functionality - Added AccountDeletion component with 30-day grace period UI - Updated Settings page with Privacy & Compliance sections COPPA Age Verification: - Added date of birth field to registration - Age calculation with COPPA compliance (under 13 blocked) - Parental email and consent for users 13-17 - Dynamic form validation based on age Privacy & Terms: - Separate checkboxes for Terms of Service and Privacy Policy - Required acceptance for registration - Links to policy pages Completes GDPR Right to Data Portability and Right to Erasure. Completes COPPA parental consent requirements.
333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { Box, Typography, Card, CardContent, TextField, Button, Divider, Switch, FormControlLabel, Alert, CircularProgress, Snackbar } from '@mui/material';
|
|
import { Save, Logout } from '@mui/icons-material';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
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 { SessionsManagement } from '@/components/settings/SessionsManagement';
|
|
import { DeviceTrustManagement } from '@/components/settings/DeviceTrustManagement';
|
|
import { BiometricSettings } from '@/components/settings/BiometricSettings';
|
|
import { DataExport } from '@/components/settings/DataExport';
|
|
import { AccountDeletion } from '@/components/settings/AccountDeletion';
|
|
import { motion } from 'framer-motion';
|
|
|
|
export default function SettingsPage() {
|
|
const { user, logout, refreshUser } = useAuth();
|
|
const [name, setName] = useState(user?.name || '');
|
|
const [settings, setSettings] = useState({
|
|
notifications: true,
|
|
emailUpdates: false,
|
|
darkMode: false,
|
|
});
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
const [nameError, setNameError] = useState<string | null>(null);
|
|
|
|
// Load preferences from user object when it changes
|
|
useEffect(() => {
|
|
if (user?.preferences) {
|
|
setSettings({
|
|
notifications: user.preferences.notifications ?? true,
|
|
emailUpdates: user.preferences.emailUpdates ?? false,
|
|
darkMode: user.preferences.darkMode ?? false,
|
|
});
|
|
}
|
|
}, [user?.preferences]);
|
|
|
|
// Sync name state when user data changes
|
|
useEffect(() => {
|
|
if (user?.name) {
|
|
setName(user.name);
|
|
}
|
|
}, [user]);
|
|
|
|
const handleSave = async () => {
|
|
// Validate name
|
|
if (!name || name.trim() === '') {
|
|
setNameError('Name cannot be empty');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
setNameError(null);
|
|
|
|
try {
|
|
const response = await usersApi.updateProfile({
|
|
name: name.trim(),
|
|
preferences: settings
|
|
});
|
|
console.log('✅ Profile updated successfully:', response);
|
|
|
|
// Refresh user to get latest data from server
|
|
await refreshUser();
|
|
|
|
setSuccessMessage('Profile updated successfully!');
|
|
} catch (err: any) {
|
|
console.error('❌ Failed to update profile:', err);
|
|
console.error('Error response:', err.response);
|
|
setError(err.response?.data?.message || err.message || 'Failed to update profile. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await logout();
|
|
};
|
|
|
|
return (
|
|
<ProtectedRoute>
|
|
<AppShell>
|
|
<Box sx={{ maxWidth: 'md', mx: 'auto' }}>
|
|
<Typography variant="h4" fontWeight="600" gutterBottom>
|
|
Settings
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
|
Manage your account settings and preferences
|
|
</Typography>
|
|
|
|
{/* Error Alert */}
|
|
{error && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<Alert severity="error" sx={{ mb: 3 }} onClose={() => setError(null)}>
|
|
{error}
|
|
</Alert>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Profile Settings */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4 }}
|
|
>
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight="600" gutterBottom>
|
|
Profile Information
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
|
|
<TextField
|
|
label="Name"
|
|
value={name}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
if (nameError) setNameError(null);
|
|
}}
|
|
fullWidth
|
|
error={!!nameError}
|
|
helperText={nameError}
|
|
disabled={isLoading}
|
|
/>
|
|
<TextField
|
|
label="Email"
|
|
value={user?.email || ''}
|
|
fullWidth
|
|
disabled
|
|
helperText="Email cannot be changed"
|
|
/>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : <Save />}
|
|
onClick={handleSave}
|
|
disabled={isLoading}
|
|
sx={{ alignSelf: 'flex-start' }}
|
|
>
|
|
{isLoading ? 'Saving...' : 'Save Changes'}
|
|
</Button>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
{/* Notification Settings */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.1 }}
|
|
>
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight="600" gutterBottom>
|
|
Notifications
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 2 }}>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={settings.notifications}
|
|
onChange={(e) => setSettings({ ...settings, notifications: e.target.checked })}
|
|
disabled={isLoading}
|
|
/>
|
|
}
|
|
label="Push Notifications"
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={settings.emailUpdates}
|
|
onChange={(e) => setSettings({ ...settings, emailUpdates: e.target.checked })}
|
|
disabled={isLoading}
|
|
/>
|
|
}
|
|
label="Email Updates"
|
|
/>
|
|
</Box>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : <Save />}
|
|
onClick={handleSave}
|
|
disabled={isLoading}
|
|
sx={{ mt: 2, alignSelf: 'flex-start' }}
|
|
>
|
|
{isLoading ? 'Saving...' : 'Save Preferences'}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
{/* Appearance Settings */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.2 }}
|
|
>
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight="600" gutterBottom>
|
|
Appearance
|
|
</Typography>
|
|
<Box sx={{ mt: 2 }}>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={settings.darkMode}
|
|
onChange={(e) => setSettings({ ...settings, darkMode: e.target.checked })}
|
|
/>
|
|
}
|
|
label="Dark Mode (Coming Soon)"
|
|
disabled
|
|
/>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
{/* Security Settings - MFA */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.25 }}
|
|
>
|
|
<Box sx={{ mb: 3 }}>
|
|
<MFASettings />
|
|
</Box>
|
|
</motion.div>
|
|
|
|
{/* Sessions Management */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.3 }}
|
|
>
|
|
<Box sx={{ mb: 3 }}>
|
|
<SessionsManagement />
|
|
</Box>
|
|
</motion.div>
|
|
|
|
{/* Device Trust Management */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.33 }}
|
|
>
|
|
<Box sx={{ mb: 3 }}>
|
|
<DeviceTrustManagement />
|
|
</Box>
|
|
</motion.div>
|
|
|
|
{/* Biometric Authentication */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.36 }}
|
|
>
|
|
<Box sx={{ mb: 3 }}>
|
|
<BiometricSettings />
|
|
</Box>
|
|
</motion.div>
|
|
|
|
{/* Privacy & Compliance - Data Export */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.39 }}
|
|
>
|
|
<Box sx={{ mb: 3 }}>
|
|
<DataExport />
|
|
</Box>
|
|
</motion.div>
|
|
|
|
{/* Privacy & Compliance - Account Deletion */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.42 }}
|
|
>
|
|
<Box sx={{ mb: 3 }}>
|
|
<AccountDeletion />
|
|
</Box>
|
|
</motion.div>
|
|
|
|
{/* Account Actions */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.45 }}
|
|
>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight="600" gutterBottom>
|
|
Account Actions
|
|
</Typography>
|
|
<Divider sx={{ my: 2 }} />
|
|
<Button
|
|
variant="outlined"
|
|
color="error"
|
|
startIcon={<Logout />}
|
|
onClick={handleLogout}
|
|
fullWidth
|
|
>
|
|
Logout
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
{/* Success Snackbar */}
|
|
<Snackbar
|
|
open={!!successMessage}
|
|
autoHideDuration={4000}
|
|
onClose={() => setSuccessMessage(null)}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
>
|
|
<Alert onClose={() => setSuccessMessage(null)} severity="success" sx={{ width: '100%' }}>
|
|
{successMessage}
|
|
</Alert>
|
|
</Snackbar>
|
|
</Box>
|
|
</AppShell>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|