feat(compliance): Implement COPPA/GDPR compliance UI

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.
This commit is contained in:
2025-10-02 17:17:06 +00:00
parent afab67da9f
commit 3335255710
5 changed files with 634 additions and 27 deletions

View File

@@ -0,0 +1,267 @@
'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>
</>
);
}

View File

@@ -0,0 +1,71 @@
'use client';
import { useState } from 'react';
import {
Card,
CardContent,
Typography,
Button,
Box,
CircularProgress,
Alert,
} from '@mui/material';
import { Download } from '@mui/icons-material';
import { complianceApi } from '@/lib/api/compliance';
export function DataExport() {
const [isExporting, setIsExporting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const handleExport = async () => {
setIsExporting(true);
setError(null);
setSuccess(null);
try {
await complianceApi.downloadUserData();
setSuccess('Your data has been downloaded successfully!');
} catch (err: any) {
console.error('Failed to export data:', err);
setError(err.response?.data?.message || 'Failed to export data. Please try again.');
} finally {
setIsExporting(false);
}
};
return (
<Card>
<CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom>
Data Export
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Download all your data in JSON format. This includes your profile, children, activities, and AI conversations.
This complies with GDPR's Right to Data Portability.
</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>
)}
<Button
variant="outlined"
startIcon={isExporting ? <CircularProgress size={20} /> : <Download />}
onClick={handleExport}
disabled={isExporting}
>
{isExporting ? 'Exporting...' : 'Download My Data'}
</Button>
</CardContent>
</Card>
);
}