feat: delete users - not tested
This commit is contained in:
@@ -3,9 +3,19 @@
|
||||
*/
|
||||
|
||||
import { apiClient } from '../../../core/api/client';
|
||||
import { UserProfile, UpdateProfileRequest } from '../types/profile.types';
|
||||
import {
|
||||
UserProfile,
|
||||
UpdateProfileRequest,
|
||||
DeletionStatus,
|
||||
RequestDeletionRequest,
|
||||
RequestDeletionResponse,
|
||||
CancelDeletionResponse,
|
||||
} from '../types/profile.types';
|
||||
|
||||
export const profileApi = {
|
||||
getProfile: () => apiClient.get<UserProfile>('/user/profile'),
|
||||
updateProfile: (data: UpdateProfileRequest) => apiClient.put<UserProfile>('/user/profile', data),
|
||||
requestDeletion: (data: RequestDeletionRequest) => apiClient.post<RequestDeletionResponse>('/user/delete', data),
|
||||
cancelDeletion: () => apiClient.post<CancelDeletionResponse>('/user/cancel-deletion'),
|
||||
getDeletionStatus: () => apiClient.get<DeletionStatus>('/user/deletion-status'),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @ai-summary Desktop dialog for requesting account deletion with 30-day grace period
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Button,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { useRequestDeletion } from '../hooks/useDeletion';
|
||||
|
||||
interface DeleteAccountDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DeleteAccountDialog: React.FC<DeleteAccountDialogProps> = ({ open, onClose }) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmationText, setConfirmationText] = useState('');
|
||||
const requestDeletionMutation = useRequestDeletion();
|
||||
|
||||
// Clear form when dialog closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setPassword('');
|
||||
setConfirmationText('');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!password || confirmationText !== 'DELETE') {
|
||||
return;
|
||||
}
|
||||
|
||||
await requestDeletionMutation.mutateAsync({ password, confirmationText });
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isValid = password.length > 0 && confirmationText === 'DELETE';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Delete Account</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="warning" sx={{ mb: 3 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
30-Day Grace Period
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Your account will be scheduled for deletion in 30 days. You can cancel this request at any time during
|
||||
the grace period by logging back in.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
autoComplete="current-password"
|
||||
helperText="Enter your password to confirm"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Type DELETE to confirm"
|
||||
value={confirmationText}
|
||||
onChange={(e) => setConfirmationText(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
helperText='Type the word "DELETE" (all caps) to confirm'
|
||||
error={confirmationText.length > 0 && confirmationText !== 'DELETE'}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={onClose} disabled={requestDeletionMutation.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={!isValid || requestDeletionMutation.isPending}
|
||||
startIcon={requestDeletionMutation.isPending ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
{requestDeletionMutation.isPending ? 'Deleting...' : 'Delete Account'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @ai-summary Desktop banner showing pending account deletion with cancel option
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Alert, AlertTitle, Button, CircularProgress, Box, Typography } from '@mui/material';
|
||||
import { useDeletionStatus, useCancelDeletion } from '../hooks/useDeletion';
|
||||
|
||||
export const PendingDeletionBanner: React.FC = () => {
|
||||
const { data: deletionStatus, isLoading } = useDeletionStatus();
|
||||
const cancelDeletionMutation = useCancelDeletion();
|
||||
|
||||
// Don't show banner if not loading and not pending deletion
|
||||
if (isLoading || !deletionStatus?.isPendingDeletion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCancelDeletion = async () => {
|
||||
await cancelDeletionMutation.mutateAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
sx={{ mb: 3 }}
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={handleCancelDeletion}
|
||||
disabled={cancelDeletionMutation.isPending}
|
||||
startIcon={cancelDeletionMutation.isPending ? <CircularProgress size={16} /> : null}
|
||||
>
|
||||
{cancelDeletionMutation.isPending ? 'Cancelling...' : 'Cancel Deletion'}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<AlertTitle>Account Deletion Pending</AlertTitle>
|
||||
<Box>
|
||||
<Typography variant="body2">
|
||||
Your account is scheduled for deletion in{' '}
|
||||
<strong>{deletionStatus.daysRemaining} {deletionStatus.daysRemaining === 1 ? 'day' : 'days'}</strong>.
|
||||
</Typography>
|
||||
{deletionStatus.deletionScheduledFor && (
|
||||
<Typography variant="body2" sx={{ mt: 0.5 }}>
|
||||
Scheduled for: {new Date(deletionStatus.deletionScheduledFor).toLocaleDateString()}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
74
frontend/src/features/settings/hooks/useDeletion.ts
Normal file
74
frontend/src/features/settings/hooks/useDeletion.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @ai-summary React hooks for account deletion functionality
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { profileApi } from '../api/profile.api';
|
||||
import { RequestDeletionRequest } from '../types/profile.types';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface ApiError {
|
||||
response?: {
|
||||
data?: {
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const useDeletionStatus = () => {
|
||||
const { isAuthenticated, isLoading } = useAuth0();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['user-deletion-status'],
|
||||
queryFn: async () => {
|
||||
const response = await profileApi.getDeletionStatus();
|
||||
return response.data;
|
||||
},
|
||||
enabled: isAuthenticated && !isLoading,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes cache time
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnMount: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRequestDeletion = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { logout } = useAuth0();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: RequestDeletionRequest) => profileApi.requestDeletion(data),
|
||||
onSuccess: (response) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['user-deletion-status'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['user-profile'] });
|
||||
toast.success(response.data.message || 'Account deletion scheduled');
|
||||
|
||||
// Logout after 2 seconds
|
||||
setTimeout(() => {
|
||||
logout({ logoutParams: { returnTo: window.location.origin } });
|
||||
}, 2000);
|
||||
},
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.error || 'Failed to request account deletion');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useCancelDeletion = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => profileApi.cancelDeletion(),
|
||||
onSuccess: (response) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['user-deletion-status'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['user-profile'] });
|
||||
queryClient.setQueryData(['user-profile'], response.data.profile);
|
||||
toast.success('Welcome back! Account deletion cancelled');
|
||||
},
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.error || 'Failed to cancel account deletion');
|
||||
},
|
||||
});
|
||||
};
|
||||
116
frontend/src/features/settings/mobile/DeleteAccountModal.tsx
Normal file
116
frontend/src/features/settings/mobile/DeleteAccountModal.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @ai-summary Mobile modal for requesting account deletion with 30-day grace period
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRequestDeletion } from '../hooks/useDeletion';
|
||||
|
||||
interface DeleteAccountModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({ isOpen, onClose }) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmationText, setConfirmationText] = useState('');
|
||||
const requestDeletionMutation = useRequestDeletion();
|
||||
|
||||
// Clear form when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setPassword('');
|
||||
setConfirmationText('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!password || confirmationText !== 'DELETE') {
|
||||
return;
|
||||
}
|
||||
|
||||
await requestDeletionMutation.mutateAsync({ password, confirmationText });
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isValid = password.length > 0 && confirmationText === 'DELETE';
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
||||
<h3 className="text-xl font-semibold text-slate-800 mb-4">Delete Account</h3>
|
||||
|
||||
{/* Warning Alert */}
|
||||
<div className="mb-4 p-4 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<p className="font-semibold text-amber-900 mb-2">30-Day Grace Period</p>
|
||||
<p className="text-sm text-amber-800">
|
||||
Your account will be scheduled for deletion in 30 days. You can cancel this request at any time during
|
||||
the grace period by logging back in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Password Input */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
style={{ fontSize: '16px', minHeight: '44px' }}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">Enter your password to confirm</p>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Input */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Type DELETE to confirm
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={confirmationText}
|
||||
onChange={(e) => setConfirmationText(e.target.value)}
|
||||
placeholder="DELETE"
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 ${
|
||||
confirmationText.length > 0 && confirmationText !== 'DELETE'
|
||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-slate-300 focus:ring-red-500 focus:border-red-500'
|
||||
}`}
|
||||
style={{ fontSize: '16px', minHeight: '44px' }}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">Type the word "DELETE" (all caps) to confirm</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={requestDeletionMutation.isPending}
|
||||
className="flex-1 py-2.5 px-4 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 transition-colors disabled:opacity-50"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValid || requestDeletionMutation.isPending}
|
||||
className="flex-1 py-2.5 px-4 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center justify-center"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
{requestDeletionMutation.isPending ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
'Delete Account'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,8 @@ import { useSettings } from '../hooks/useSettings';
|
||||
import { useProfile, useUpdateProfile } from '../hooks/useProfile';
|
||||
import { useAdminAccess } from '../../../core/auth/useAdminAccess';
|
||||
import { useNavigationStore } from '../../../core/store';
|
||||
import { DeleteAccountModal } from './DeleteAccountModal';
|
||||
import { PendingDeletionBanner } from './PendingDeletionBanner';
|
||||
|
||||
interface ToggleSwitchProps {
|
||||
enabled: boolean;
|
||||
@@ -105,11 +107,6 @@ export const MobileSettingsScreen: React.FC = () => {
|
||||
setShowDataExport(false);
|
||||
};
|
||||
|
||||
const handleDeleteAccount = () => {
|
||||
// TODO: Implement account deletion
|
||||
console.log('Deleting account...');
|
||||
setShowDeleteConfirm(false);
|
||||
};
|
||||
|
||||
const handleEditProfile = () => {
|
||||
setIsEditingProfile(true);
|
||||
@@ -190,6 +187,9 @@ export const MobileSettingsScreen: React.FC = () => {
|
||||
<p className="text-slate-500 mt-2">Manage your account and preferences</p>
|
||||
</div>
|
||||
|
||||
{/* Pending Deletion Banner */}
|
||||
<PendingDeletionBanner />
|
||||
|
||||
{/* Profile Section */}
|
||||
<GlassCard padding="md">
|
||||
<div>
|
||||
@@ -488,30 +488,11 @@ export const MobileSettingsScreen: React.FC = () => {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Delete Account Confirmation */}
|
||||
<Modal
|
||||
{/* Delete Account Modal */}
|
||||
<DeleteAccountModal
|
||||
isOpen={showDeleteConfirm}
|
||||
onClose={() => setShowDeleteConfirm(false)}
|
||||
title="Delete Account"
|
||||
>
|
||||
<p className="text-slate-600 mb-4">
|
||||
This action cannot be undone. All your data will be permanently deleted.
|
||||
</p>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
className="flex-1 py-2 px-4 bg-gray-200 text-gray-700 rounded-lg font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteAccount}
|
||||
className="flex-1 py-2 px-4 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
/>
|
||||
</div>
|
||||
</MobileContainer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @ai-summary Mobile banner showing pending account deletion with cancel option
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
||||
import { useDeletionStatus, useCancelDeletion } from '../hooks/useDeletion';
|
||||
|
||||
export const PendingDeletionBanner: React.FC = () => {
|
||||
const { data: deletionStatus, isLoading } = useDeletionStatus();
|
||||
const cancelDeletionMutation = useCancelDeletion();
|
||||
|
||||
// Don't show banner if not loading and not pending deletion
|
||||
if (isLoading || !deletionStatus?.isPendingDeletion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCancelDeletion = async () => {
|
||||
await cancelDeletionMutation.mutateAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
<GlassCard padding="md" className="bg-amber-50/80 border-amber-200/70">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-amber-900 mb-1">Account Deletion Pending</h3>
|
||||
<p className="text-sm text-amber-800">
|
||||
Your account is scheduled for deletion in{' '}
|
||||
<strong>{deletionStatus.daysRemaining} {deletionStatus.daysRemaining === 1 ? 'day' : 'days'}</strong>.
|
||||
</p>
|
||||
{deletionStatus.deletionScheduledFor && (
|
||||
<p className="text-xs text-amber-700 mt-1">
|
||||
Scheduled for: {new Date(deletionStatus.deletionScheduledFor).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCancelDeletion}
|
||||
disabled={cancelDeletionMutation.isPending}
|
||||
className="w-full py-2.5 px-4 bg-amber-600 text-white rounded-lg font-medium hover:bg-amber-700 transition-colors disabled:opacity-50 flex items-center justify-center"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
{cancelDeletionMutation.isPending ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
'Cancel Deletion'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
);
|
||||
};
|
||||
@@ -16,3 +16,26 @@ export interface UpdateProfileRequest {
|
||||
displayName?: string;
|
||||
notificationEmail?: string;
|
||||
}
|
||||
|
||||
export interface DeletionStatus {
|
||||
isPendingDeletion: boolean;
|
||||
deletionRequestedAt: string | null;
|
||||
deletionScheduledFor: string | null;
|
||||
daysRemaining: number | null;
|
||||
}
|
||||
|
||||
export interface RequestDeletionRequest {
|
||||
password: string;
|
||||
confirmationText: string;
|
||||
}
|
||||
|
||||
export interface RequestDeletionResponse {
|
||||
message: string;
|
||||
deletionScheduledFor: string;
|
||||
daysRemaining: number;
|
||||
}
|
||||
|
||||
export interface CancelDeletionResponse {
|
||||
message: string;
|
||||
profile: UserProfile;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user