diff --git a/backend/routers/users.py b/backend/routers/users.py index 5d09251..6737649 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -270,6 +270,136 @@ def get_user_groups( ).all() return groups +# --- GDPR Data Export --- + +@router.get("/me/export") +def export_my_data( + current_user: User = Depends(get_current_user), + session: Session = Depends(get_session) +): + """Export all user data for GDPR compliance""" + from models import Rating, Comment, Notification, Badge + + # Get profile + profile = session.exec(select(Profile).where(Profile.user_id == current_user.id)).first() + + # Get preferences + prefs = current_user.preferences + + # Get attendance + attendance = session.exec( + select(Attendance, Show) + .join(Show, Attendance.show_id == Show.id) + .where(Attendance.user_id == current_user.id) + ).all() + + # Get reviews + reviews = session.exec( + select(Review).where(Review.user_id == current_user.id) + ).all() + + # Get ratings + ratings = session.exec( + select(Rating).where(Rating.user_id == current_user.id) + ).all() + + # Get groups + groups = session.exec( + select(Group) + .join(GroupMember, Group.id == GroupMember.group_id) + .where(GroupMember.user_id == current_user.id) + ).all() + + return { + "account": { + "id": current_user.id, + "email": current_user.email, + "joined_at": current_user.joined_at.isoformat() if current_user.joined_at else None, + "xp": current_user.xp, + "level": current_user.level, + }, + "profile": { + "username": profile.username if profile else None, + "display_name": profile.display_name if profile else None, + "bio": current_user.bio, + "avatar_bg_color": current_user.avatar_bg_color, + "avatar_text": current_user.avatar_text, + }, + "preferences": { + "wiki_mode": prefs.wiki_mode if prefs else False, + "show_ratings": prefs.show_ratings if prefs else True, + "show_comments": prefs.show_comments if prefs else True, + "theme": prefs.theme if prefs else "system", + }, + "privacy": { + "profile_public": current_user.profile_public, + "show_attendance_public": current_user.show_attendance_public, + "appear_in_leaderboards": current_user.appear_in_leaderboards, + }, + "attendance": [ + {"show_id": a.show_id, "date": s.date.isoformat() if s.date else None} + for a, s in attendance + ], + "reviews": [ + {"id": r.id, "score": r.score, "blurb": r.blurb, "content": r.content, "created_at": r.created_at.isoformat() if r.created_at else None} + for r in reviews + ], + "ratings": [ + {"id": r.id, "score": r.score, "performance_id": r.performance_id, "show_id": r.show_id} + for r in ratings + ], + "groups": [ + {"id": g.id, "name": g.name} + for g in groups + ], + } + +# --- Account Deletion --- + +class DeleteAccountRequest(BaseModel): + confirm_email: str + +@router.delete("/me") +def delete_my_account( + request: DeleteAccountRequest, + current_user: User = Depends(get_current_user), + session: Session = Depends(get_session) +): + """Permanently delete user account and all associated data""" + from models import Rating, Comment, Notification, Badge + + # Verify email confirmation matches + if request.confirm_email.lower() != current_user.email.lower(): + raise HTTPException(status_code=400, detail="Email confirmation doesn't match") + + user_id = current_user.id + + # Delete related data in order (foreign key constraints) + session.exec(select(Review).where(Review.user_id == user_id)).all() + for review in session.exec(select(Review).where(Review.user_id == user_id)).all(): + session.delete(review) + + for rating in session.exec(select(Rating).where(Rating.user_id == user_id)).all(): + session.delete(rating) + + for attendance in session.exec(select(Attendance).where(Attendance.user_id == user_id)).all(): + session.delete(attendance) + + for membership in session.exec(select(GroupMember).where(GroupMember.user_id == user_id)).all(): + session.delete(membership) + + for profile in session.exec(select(Profile).where(Profile.user_id == user_id)).all(): + session.delete(profile) + + if current_user.preferences: + session.delete(current_user.preferences) + + # Finally delete the user + session.delete(current_user) + session.commit() + + return {"message": "Account deleted successfully"} + # --- Dynamic ID Routes (must be last to avoid conflicts with /me, /avatar) --- @router.get("/{user_id}", response_model=UserRead) diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index 65b72e6..d97b0f3 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -661,10 +661,70 @@ function PrivacySection({ settings, onChange }: {

Danger Zone

- -