feat: user export service. bug and UX fixes. Complete minus outstanding email template fixes.

This commit is contained in:
Eric Gullickson
2025-12-26 14:06:03 -06:00
parent 8c13dc0a55
commit fb52ce398b
35 changed files with 1686 additions and 118 deletions

View 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>
);
};

View File

@@ -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>