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.
268 lines
9.0 KiB
TypeScript
268 lines
9.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
Typography,
|
|
Button,
|
|
Box,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogActions,
|
|
TextField,
|
|
Alert,
|
|
CircularProgress,
|
|
Chip,
|
|
} from '@mui/material';
|
|
import { DeleteForever, Cancel, Warning } from '@mui/icons-material';
|
|
import { complianceApi, DeletionRequest } from '@/lib/api/compliance';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
|
|
export function AccountDeletion() {
|
|
const { logout } = useAuth();
|
|
const [deletionRequest, setDeletionRequest] = useState<DeletionRequest | null>(null);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
|
|
const [reason, setReason] = useState('');
|
|
const [cancellationReason, setCancellationReason] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isCheckingStatus, setIsCheckingStatus] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
checkDeletionStatus();
|
|
}, []);
|
|
|
|
const checkDeletionStatus = async () => {
|
|
setIsCheckingStatus(true);
|
|
try {
|
|
const status = await complianceApi.getDeletionStatus();
|
|
setDeletionRequest(status);
|
|
} catch (err) {
|
|
console.error('Failed to check deletion status:', err);
|
|
} finally {
|
|
setIsCheckingStatus(false);
|
|
}
|
|
};
|
|
|
|
const handleRequestDeletion = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
setSuccess(null);
|
|
|
|
try {
|
|
const request = await complianceApi.requestAccountDeletion(reason);
|
|
setDeletionRequest(request);
|
|
setSuccess(
|
|
`Account deletion scheduled for ${new Date(request.scheduledDeletionAt).toLocaleDateString()}. You can cancel this request within 30 days.`
|
|
);
|
|
setIsDialogOpen(false);
|
|
setReason('');
|
|
} catch (err: any) {
|
|
console.error('Failed to request deletion:', err);
|
|
setError(err.response?.data?.message || 'Failed to request account deletion. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCancelDeletion = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
setSuccess(null);
|
|
|
|
try {
|
|
await complianceApi.cancelAccountDeletion(cancellationReason);
|
|
setDeletionRequest(null);
|
|
setSuccess('Account deletion request has been cancelled successfully.');
|
|
setIsCancelDialogOpen(false);
|
|
setCancellationReason('');
|
|
} catch (err: any) {
|
|
console.error('Failed to cancel deletion:', err);
|
|
setError(err.response?.data?.message || 'Failed to cancel account deletion. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
if (isCheckingStatus) {
|
|
return (
|
|
<Card>
|
|
<CardContent>
|
|
<Box display="flex" justifyContent="center" alignItems="center" py={3}>
|
|
<CircularProgress size={30} />
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight="600" gutterBottom color="error">
|
|
Danger Zone
|
|
</Typography>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{success && (
|
|
<Alert severity="success" sx={{ mb: 2 }} onClose={() => setSuccess(null)}>
|
|
{success}
|
|
</Alert>
|
|
)}
|
|
|
|
{deletionRequest?.status === 'pending' ? (
|
|
<Box>
|
|
<Alert severity="warning" icon={<Warning />} sx={{ mb: 2 }}>
|
|
<Typography variant="body2" fontWeight="600" gutterBottom>
|
|
Account Deletion Scheduled
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
Your account is scheduled for deletion on{' '}
|
|
<strong>{new Date(deletionRequest.scheduledDeletionAt).toLocaleDateString()}</strong>.
|
|
You can cancel this request at any time before that date.
|
|
</Typography>
|
|
{deletionRequest.reason && (
|
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
|
Reason: {deletionRequest.reason}
|
|
</Typography>
|
|
)}
|
|
</Alert>
|
|
|
|
<Box display="flex" gap={2}>
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<Cancel />}
|
|
onClick={() => setIsCancelDialogOpen(true)}
|
|
disabled={isLoading}
|
|
>
|
|
Cancel Deletion Request
|
|
</Button>
|
|
<Chip
|
|
label={`${Math.ceil((new Date(deletionRequest.scheduledDeletionAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))} days remaining`}
|
|
color="warning"
|
|
size="small"
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
) : (
|
|
<Box>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
Permanently delete your account and all associated data. This action cannot be undone after the 30-day grace period.
|
|
This complies with GDPR's Right to Erasure.
|
|
</Typography>
|
|
<Typography variant="body2" color="error" sx={{ mb: 2 }}>
|
|
<strong>Warning:</strong> Deleting your account will remove all your data including children profiles, activities,
|
|
photos, and AI conversations. This data cannot be recovered.
|
|
</Typography>
|
|
|
|
<Button
|
|
variant="outlined"
|
|
color="error"
|
|
startIcon={<DeleteForever />}
|
|
onClick={() => setIsDialogOpen(true)}
|
|
>
|
|
Delete My Account
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Delete Account Dialog */}
|
|
<Dialog open={isDialogOpen} onClose={() => !isLoading && setIsDialogOpen(false)} maxWidth="sm" fullWidth>
|
|
<DialogTitle>Delete Account</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText sx={{ mb: 2 }}>
|
|
Are you sure you want to delete your account? This will permanently remove all your data after a 30-day grace period.
|
|
</DialogContentText>
|
|
<DialogContentText sx={{ mb: 2 }} color="error">
|
|
<strong>This action will delete:</strong>
|
|
</DialogContentText>
|
|
<ul style={{ marginTop: 0 }}>
|
|
<li>Your profile and account information</li>
|
|
<li>All children profiles and their data</li>
|
|
<li>All activity tracking records</li>
|
|
<li>All photos and attachments</li>
|
|
<li>All AI conversation history</li>
|
|
<li>All family memberships</li>
|
|
</ul>
|
|
<TextField
|
|
fullWidth
|
|
multiline
|
|
rows={3}
|
|
label="Reason for deletion (optional)"
|
|
value={reason}
|
|
onChange={(e) => setReason(e.target.value)}
|
|
placeholder="Help us improve by telling us why you're leaving..."
|
|
sx={{ mt: 2 }}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setIsDialogOpen(false)} disabled={isLoading}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleRequestDeletion}
|
|
color="error"
|
|
variant="contained"
|
|
disabled={isLoading}
|
|
startIcon={isLoading ? <CircularProgress size={20} /> : <DeleteForever />}
|
|
>
|
|
{isLoading ? 'Processing...' : 'Delete Account'}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Cancel Deletion Dialog */}
|
|
<Dialog
|
|
open={isCancelDialogOpen}
|
|
onClose={() => !isLoading && setIsCancelDialogOpen(false)}
|
|
maxWidth="sm"
|
|
fullWidth
|
|
>
|
|
<DialogTitle>Cancel Account Deletion</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText sx={{ mb: 2 }}>
|
|
Are you sure you want to cancel your account deletion request? Your account will remain active.
|
|
</DialogContentText>
|
|
<TextField
|
|
fullWidth
|
|
multiline
|
|
rows={2}
|
|
label="Reason for cancellation (optional)"
|
|
value={cancellationReason}
|
|
onChange={(e) => setCancellationReason(e.target.value)}
|
|
placeholder="Tell us what made you change your mind..."
|
|
sx={{ mt: 2 }}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setIsCancelDialogOpen(false)} disabled={isLoading}>
|
|
Close
|
|
</Button>
|
|
<Button
|
|
onClick={handleCancelDeletion}
|
|
color="primary"
|
|
variant="contained"
|
|
disabled={isLoading}
|
|
startIcon={isLoading ? <CircularProgress size={20} /> : <Cancel />}
|
|
>
|
|
{isLoading ? 'Processing...' : 'Cancel Deletion'}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|