feat: user export service. bug and UX fixes. Complete minus outstanding email template fixes.
This commit is contained in:
223
frontend/src/pages/SecuritySettingsPage.tsx
Normal file
223
frontend/src/pages/SecuritySettingsPage.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* @ai-summary Security settings page for desktop application
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSecurityStatus, useRequestPasswordReset } from '../features/settings/hooks/useSecurity';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Button as MuiButton,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
|
||||
import FingerprintIcon from '@mui/icons-material/Fingerprint';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import { Card } from '../shared-minimal/components/Card';
|
||||
|
||||
export const SecuritySettingsPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data: securityStatus, isLoading, error } = useSecurityStatus();
|
||||
const passwordResetMutation = useRequestPasswordReset();
|
||||
|
||||
const handlePasswordReset = () => {
|
||||
passwordResetMutation.mutate();
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
navigate('/garage/settings');
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4 }}>
|
||||
<MuiButton
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Back
|
||||
</MuiButton>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||
Security Settings
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4 }}>
|
||||
<MuiButton
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Back
|
||||
</MuiButton>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||
Security Settings
|
||||
</Typography>
|
||||
</Box>
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
Failed to load security settings. Please try again.
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4 }}>
|
||||
<MuiButton
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Back
|
||||
</MuiButton>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||
Security Settings
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{/* Email Verification Status */}
|
||||
<Card>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
|
||||
Account Verification
|
||||
</Typography>
|
||||
|
||||
<List disablePadding>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<EmailIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Email Address"
|
||||
secondary={securityStatus?.email || 'Not available'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<VerifiedUserIcon color={securityStatus?.emailVerified ? 'success' : 'warning'} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Email Verification"
|
||||
secondary={
|
||||
securityStatus?.emailVerified
|
||||
? 'Your email address has been verified'
|
||||
: 'Your email address is not verified'
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label={securityStatus?.emailVerified ? 'Verified' : 'Not Verified'}
|
||||
color={securityStatus?.emailVerified ? 'success' : 'warning'}
|
||||
size="small"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Card>
|
||||
|
||||
{/* Password Management */}
|
||||
<Card>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
|
||||
Password
|
||||
</Typography>
|
||||
|
||||
<List disablePadding>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<LockIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Change Password"
|
||||
secondary="Receive an email with a link to reset your password"
|
||||
/>
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handlePasswordReset}
|
||||
disabled={passwordResetMutation.isPending}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{passwordResetMutation.isPending ? (
|
||||
<CircularProgress size={20} color="inherit" />
|
||||
) : (
|
||||
'Reset Password'
|
||||
)}
|
||||
</MuiButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
{passwordResetMutation.isSuccess && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
Password reset email sent! Please check your inbox.
|
||||
</Alert>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Passkeys Information */}
|
||||
<Card>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
|
||||
Passkeys
|
||||
</Typography>
|
||||
|
||||
<List disablePadding>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<FingerprintIcon color={securityStatus?.passkeysEnabled ? 'primary' : 'disabled'} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Passkey Authentication"
|
||||
secondary={
|
||||
securityStatus?.passkeysEnabled
|
||||
? 'Passkeys are available for your account'
|
||||
: 'Passkeys are not enabled'
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label={securityStatus?.passkeysEnabled ? 'Available' : 'Not Available'}
|
||||
color={securityStatus?.passkeysEnabled ? 'primary' : 'default'}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>About Passkeys:</strong> Passkeys are a secure, passwordless way to sign in using your device's biometric authentication (fingerprint, face recognition) or PIN.
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
You can register a passkey during the sign-in process. When you see the option to "Create a passkey," follow the prompts to set up passwordless authentication.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useUnits } from '../core/units/UnitsContext';
|
||||
import { useAdminAccess } from '../core/auth/useAdminAccess';
|
||||
import { useProfile, useUpdateProfile } from '../features/settings/hooks/useProfile';
|
||||
import { useExportUserData } from '../features/settings/hooks/useExportUserData';
|
||||
import { useTheme } from '../shared-minimal/theme/ThemeContext';
|
||||
import { DeleteAccountDialog } from '../features/settings/components/DeleteAccountDialog';
|
||||
import { PendingDeletionBanner } from '../features/settings/components/PendingDeletionBanner';
|
||||
@@ -56,6 +57,7 @@ export const SettingsPage: React.FC = () => {
|
||||
const [editedDisplayName, setEditedDisplayName] = useState('');
|
||||
const [editedNotificationEmail, setEditedNotificationEmail] = useState('');
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const exportMutation = useExportUserData();
|
||||
|
||||
// Initialize edit form when profile loads or edit mode starts
|
||||
React.useEffect(() => {
|
||||
@@ -120,10 +122,17 @@ export const SettingsPage: React.FC = () => {
|
||||
</Typography>
|
||||
{!isEditingProfile && !profileLoading && (
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<EditIcon />}
|
||||
onClick={handleEditProfile}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</MuiButton>
|
||||
@@ -246,7 +255,18 @@ export const SettingsPage: React.FC = () => {
|
||||
secondary="Password, two-factor authentication"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<MuiButton variant="outlined" size="small">
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => navigate('/garage/settings/security')}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MuiButton>
|
||||
</ListItemSecondaryAction>
|
||||
@@ -366,21 +386,20 @@ export const SettingsPage: React.FC = () => {
|
||||
secondary="Download your vehicle and fuel log data"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<MuiButton variant="outlined" size="small">
|
||||
Export
|
||||
</MuiButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Clear Cache"
|
||||
secondary="Remove cached data to free up space"
|
||||
sx={{ pl: 7 }}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<MuiButton variant="outlined" size="small" color="warning">
|
||||
Clear
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={exportMutation.isPending}
|
||||
onClick={() => exportMutation.mutate()}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{exportMutation.isPending ? 'Exporting...' : 'Export'}
|
||||
</MuiButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
@@ -405,9 +424,16 @@ export const SettingsPage: React.FC = () => {
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => navigate('/garage/settings/admin/users')}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MuiButton>
|
||||
@@ -422,9 +448,16 @@ export const SettingsPage: React.FC = () => {
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => navigate('/garage/settings/admin/catalog')}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MuiButton>
|
||||
@@ -439,9 +472,16 @@ export const SettingsPage: React.FC = () => {
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => navigate('/garage/settings/admin/email-templates')}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MuiButton>
|
||||
@@ -456,9 +496,16 @@ export const SettingsPage: React.FC = () => {
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => navigate('/garage/settings/admin/backup')}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MuiButton>
|
||||
@@ -470,16 +517,22 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
{/* Account Actions */}
|
||||
<Card>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, color: 'error.main' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, color: 'primary.main' }}>
|
||||
Account Actions
|
||||
</Typography>
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
onClick={handleLogout}
|
||||
sx={{ borderRadius: '999px' }}
|
||||
sx={{
|
||||
borderRadius: '999px',
|
||||
backgroundColor: 'grey.600',
|
||||
color: 'common.white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'grey.700'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Sign Out
|
||||
</MuiButton>
|
||||
|
||||
Reference in New Issue
Block a user