Add GDPR data export and account deletion
This commit is contained in:
parent
80c686da53
commit
7535f670d8
2 changed files with 192 additions and 2 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -661,10 +661,70 @@ function PrivacySection({ settings, onChange }: {
|
|||
<div className="pt-4">
|
||||
<h4 className="text-sm font-medium mb-2">Danger Zone</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="outline" size="sm" className="w-fit" disabled>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
onClick={async () => {
|
||||
const token = localStorage.getItem("token")
|
||||
if (!token) return
|
||||
try {
|
||||
const res = await fetch(`${getApiUrl()}/users/me/export`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'my-elmeg-data.json'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Export failed', err)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Export My Data
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" className="w-fit" disabled>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
onClick={async () => {
|
||||
const email = prompt("To delete your account, please type your email address:")
|
||||
if (!email) return
|
||||
const confirm = window.confirm("Are you absolutely sure? This action cannot be undone. All your data will be permanently deleted.")
|
||||
if (!confirm) return
|
||||
|
||||
const token = localStorage.getItem("token")
|
||||
if (!token) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`${getApiUrl()}/users/me`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ confirm_email: email })
|
||||
})
|
||||
if (res.ok) {
|
||||
alert("Account deleted. Goodbye!")
|
||||
localStorage.removeItem("token")
|
||||
window.location.href = "/"
|
||||
} else {
|
||||
const data = await res.json()
|
||||
alert(data.detail || "Deletion failed")
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Deletion failed', err)
|
||||
alert("Error deleting account")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue