diff --git a/maternal-web/app/(auth)/register/page.tsx b/maternal-web/app/(auth)/register/page.tsx index 29666de..824b5fc 100644 --- a/maternal-web/app/(auth)/register/page.tsx +++ b/maternal-web/app/(auth)/register/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { Box, TextField, @@ -32,12 +32,39 @@ const registerSchema = z.object({ .regex(/[a-z]/, 'Password must contain at least one lowercase letter') .regex(/[0-9]/, 'Password must contain at least one number'), confirmPassword: z.string(), + dateOfBirth: z.string().min(1, 'Date of birth is required'), + parentalEmail: z.string().email('Invalid email address').optional().or(z.literal('')), agreeToTerms: z.boolean().refine(val => val === true, { - message: 'You must agree to the terms and conditions', + message: 'You must agree to the Terms of Service', }), + agreeToPrivacy: z.boolean().refine(val => val === true, { + message: 'You must agree to the Privacy Policy', + }), + coppaConsent: z.boolean().optional(), }).refine((data) => data.password === data.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'], +}).refine((data) => { + // Check if user is under 18 and requires parental consent + const birthDate = new Date(data.dateOfBirth); + const today = new Date(); + const age = today.getFullYear() - birthDate.getFullYear() - + (today.getMonth() < birthDate.getMonth() || + (today.getMonth() === birthDate.getMonth() && today.getDate() < birthDate.getDate()) ? 1 : 0); + + if (age < 13) { + return false; // Users under 13 cannot register (COPPA compliance) + } + + if (age >= 13 && age < 18) { + // Users 13-17 need parental email and consent + return !!data.parentalEmail && data.parentalEmail.length > 0 && data.coppaConsent === true; + } + + return true; +}, { + message: 'Users under 13 cannot create an account. Users 13-17 require parental consent and email.', + path: ['dateOfBirth'], }); type RegisterFormData = z.infer; @@ -47,16 +74,50 @@ export default function RegisterPage() { const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [userAge, setUserAge] = useState(null); + const [requiresParentalConsent, setRequiresParentalConsent] = useState(false); const { register: registerUser } = useAuth(); const { register, handleSubmit, + watch, formState: { errors }, } = useForm({ resolver: zodResolver(registerSchema), + defaultValues: { + agreeToTerms: false, + agreeToPrivacy: false, + coppaConsent: false, + }, }); + // Watch date of birth to calculate age and show parental consent if needed + const dateOfBirth = watch('dateOfBirth'); + + // Calculate age when date of birth changes + const calculateAge = (dob: string): number | null => { + if (!dob) return null; + const birthDate = new Date(dob); + const today = new Date(); + const age = today.getFullYear() - birthDate.getFullYear() - + (today.getMonth() < birthDate.getMonth() || + (today.getMonth() === birthDate.getMonth() && today.getDate() < birthDate.getDate()) ? 1 : 0); + return age; + }; + + // Update age and parental consent requirement when DOB changes + React.useEffect(() => { + if (dateOfBirth) { + const age = calculateAge(dateOfBirth); + setUserAge(age); + setRequiresParentalConsent(age !== null && age >= 13 && age < 18); + } else { + setUserAge(null); + setRequiresParentalConsent(false); + } + }, [dateOfBirth]); + const onSubmit = async (data: RegisterFormData) => { setError(null); setIsLoading(true); @@ -66,6 +127,9 @@ export default function RegisterPage() { name: data.name, email: data.email, password: data.password, + dateOfBirth: data.dateOfBirth, + parentalEmail: data.parentalEmail || undefined, + coppaConsentGiven: data.coppaConsent || false, }); // Navigation to onboarding is handled in the register function } catch (err: any) { @@ -208,33 +272,115 @@ export default function RegisterPage() { }} /> - - } - label={ - - I agree to the{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - - - } - sx={{ mt: 2 }} + - {errors.agreeToTerms && ( - - {errors.agreeToTerms.message} - + + {userAge !== null && userAge < 13 && ( + + Users under 13 years old cannot create an account per COPPA regulations. + )} + {requiresParentalConsent && ( + + + As you are under 18, parental consent is required to create an account. + + + + } + label={ + + I confirm that I have my parent/guardian's permission to create this account + + } + sx={{ mt: 1 }} + /> + {errors.coppaConsent && ( + + Parental consent is required for users under 18 + + )} + + )} + + + + } + label={ + + I agree to the{' '} + + Terms of Service + + + } + /> + {errors.agreeToTerms && ( + + {errors.agreeToTerms.message} + + )} + + + } + label={ + + I agree to the{' '} + + Privacy Policy + + + } + /> + {errors.agreeToPrivacy && ( + + {errors.agreeToPrivacy.message} + + )} + + + + + + ) : ( + + + 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. + + + Warning: Deleting your account will remove all your data including children profiles, activities, + photos, and AI conversations. This data cannot be recovered. + + + + + )} + + + + {/* Delete Account Dialog */} + !isLoading && setIsDialogOpen(false)} maxWidth="sm" fullWidth> + Delete Account + + + Are you sure you want to delete your account? This will permanently remove all your data after a 30-day grace period. + + + This action will delete: + +
    +
  • Your profile and account information
  • +
  • All children profiles and their data
  • +
  • All activity tracking records
  • +
  • All photos and attachments
  • +
  • All AI conversation history
  • +
  • All family memberships
  • +
+ setReason(e.target.value)} + placeholder="Help us improve by telling us why you're leaving..." + sx={{ mt: 2 }} + /> +
+ + + + +
+ + {/* Cancel Deletion Dialog */} + !isLoading && setIsCancelDialogOpen(false)} + maxWidth="sm" + fullWidth + > + Cancel Account Deletion + + + Are you sure you want to cancel your account deletion request? Your account will remain active. + + setCancellationReason(e.target.value)} + placeholder="Tell us what made you change your mind..." + sx={{ mt: 2 }} + /> + + + + + + + + ); +} diff --git a/maternal-web/components/settings/DataExport.tsx b/maternal-web/components/settings/DataExport.tsx new file mode 100644 index 0000000..f94b712 --- /dev/null +++ b/maternal-web/components/settings/DataExport.tsx @@ -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(null); + const [success, setSuccess] = useState(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 ( + + + + Data Export + + + 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. + + + {error && ( + setError(null)}> + {error} + + )} + + {success && ( + setSuccess(null)}> + {success} + + )} + + + + + ); +} diff --git a/maternal-web/lib/api/compliance.ts b/maternal-web/lib/api/compliance.ts new file mode 100644 index 0000000..52ed097 --- /dev/null +++ b/maternal-web/lib/api/compliance.ts @@ -0,0 +1,99 @@ +import { apiClient } from './client'; + +export interface UserDataExport { + user: { + id: string; + name: string; + email: string; + dateOfBirth?: string; + createdAt: string; + }; + families: Array<{ + id: string; + name: string; + role: string; + }>; + children: Array<{ + id: string; + name: string; + dateOfBirth: string; + gender: string; + }>; + activities: Array<{ + id: string; + type: string; + timestamp: string; + data: any; + }>; + aiConversations: Array<{ + id: string; + createdAt: string; + messages: Array<{ + role: string; + content: string; + timestamp: string; + }>; + }>; +} + +export interface DeletionRequest { + id: string; + requestedAt: string; + scheduledDeletionAt: string; + status: 'pending' | 'cancelled' | 'completed'; + reason?: string; +} + +export const complianceApi = { + /** + * Export all user data (GDPR Right to Data Portability) + */ + exportUserData: async (): Promise => { + const response = await apiClient.get<{ data: UserDataExport }>('/compliance/data-export'); + return response.data.data; + }, + + /** + * Download user data as JSON file + */ + downloadUserData: async (): Promise => { + const data = await complianceApi.exportUserData(); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `maternal-app-data-export-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }, + + /** + * Request account deletion with 30-day grace period (GDPR Right to Erasure) + */ + requestAccountDeletion: async (reason?: string): Promise => { + const response = await apiClient.post<{ data: DeletionRequest }>('/compliance/request-deletion', { + reason, + }); + return response.data.data; + }, + + /** + * Cancel pending account deletion request + */ + cancelAccountDeletion: async (cancellationReason?: string): Promise => { + const response = await apiClient.post<{ data: DeletionRequest }>('/compliance/cancel-deletion', { + cancellationReason, + }); + return response.data.data; + }, + + /** + * Get pending deletion request status + */ + getDeletionStatus: async (): Promise => { + const response = await apiClient.get<{ data: DeletionRequest | null }>('/compliance/deletion-status'); + return response.data.data; + }, +};