package db import ( "context" "fmt" "log/slog" ) // DeleteUserDataFull performs complete user deletion including non-cascading tables. // This is the main function for GDPR account deletion. // // Order of operations: // 1. Delete hold membership data (non-cascading tables) // 2. Delete OAuth sessions // 3. Delete user (cascades to manifests, tags, stars, repo_pages, etc.) // // This should be called AFTER remote cleanup (hold services, PDS records) // since we need the OAuth tokens to authenticate those requests. func DeleteUserDataFull(db DBTX, oauthStore *OAuthStore, did string) error { slog.Info("Starting full user data deletion", "did", did) // 1. Delete non-cascading hold membership tables if err := deleteHoldMembershipData(db, did); err != nil { slog.Error("Failed to delete hold membership data", "did", did, "error", err) return fmt.Errorf("failed to delete hold membership data: %w", err) } // 2. Delete OAuth sessions if oauthStore != nil { if err := oauthStore.DeleteSessionsForDID(context.Background(), did); err != nil { slog.Warn("Failed to delete OAuth sessions", "did", did, "error", err) // Continue - not critical } else { slog.Debug("Deleted OAuth sessions", "did", did) } } // 3. Delete user (cascades to manifests, tags, stars, annotations, etc.) if _, err := DeleteUserData(db, did); err != nil { slog.Error("Failed to delete user data", "did", did, "error", err) return fmt.Errorf("failed to delete user data: %w", err) } slog.Info("User data deletion completed", "did", did) return nil } // deleteHoldMembershipData deletes non-cascading hold membership tables. // These tables don't have foreign keys to the users table. func deleteHoldMembershipData(db DBTX, did string) error { // Delete from hold_crew_approvals (where user is the approved member) result, err := db.Exec(`DELETE FROM hold_crew_approvals WHERE user_did = ?`, did) if err != nil { return fmt.Errorf("failed to delete crew approvals: %w", err) } approvalsDeleted, _ := result.RowsAffected() // Delete from hold_crew_denials (where user was denied) result, err = db.Exec(`DELETE FROM hold_crew_denials WHERE user_did = ?`, did) if err != nil { return fmt.Errorf("failed to delete crew denials: %w", err) } denialsDeleted, _ := result.RowsAffected() // Delete from hold_crew_members (cached crew memberships) result, err = db.Exec(`DELETE FROM hold_crew_members WHERE member_did = ?`, did) if err != nil { return fmt.Errorf("failed to delete crew members: %w", err) } membersDeleted, _ := result.RowsAffected() slog.Debug("Deleted hold membership data", "did", did, "approvals_deleted", approvalsDeleted, "denials_deleted", denialsDeleted, "members_deleted", membersDeleted) return nil }