package user import ( "context" "database/sql" "errors" "fmt" "log" "net/http" "os" "crussell/db" "crussell/handlers/payments" "crussell/internal/dav" "crussell/internal/s3" "crussell/mw" "github.com/jackc/pgx/v5" ) // DELETE /api/user/account func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) { userID, ok := mw.GetUserID(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var accountRole string var profilePicURL sql.NullString err := db.DB.QueryRow(r.Context(), `SELECT account_role, profile_pic_url FROM users WHERE id = $1`, userID). Scan(&accountRole, &profilePicURL) if err != nil { if errors.Is(err, pgx.ErrNoRows) { http.Error(w, "user not found", http.StatusNotFound) return } log.Printf("Failed to fetch user for deletion: %v", err) http.Error(w, "server error", http.StatusInternalServerError) return } ctx := r.Context() // --- External system scrubbing (BEFORE SQL anonymize) --- // Delete profile picture from S3/R2 if profilePicURL.Valid && profilePicURL.String != "" && s3.Client != nil { go func(picURL string) { bucket := os.Getenv("S3_PROFILE_PICS_BUCKET") if bucket == "" { bucket = "crussell-profile-pics" } // profiles/{userID}.jpg — matches UploadProfilePictureHandler key format key := fmt.Sprintf("profiles/%s.jpg", userID) if err := s3.Client.Delete(context.Background(), bucket, key); err != nil { log.Printf("Warning: Failed to delete profile picture for user %s: %v", userID, err) } }(profilePicURL.String) } if payments.SquareClient != nil { go func() { rows, err := db.DB.Query(ctx, `SELECT square_card_id FROM user_saved_cards WHERE user_id = $1 AND deleted_at IS NULL`, userID) if err != nil { log.Printf("Warning: Failed to query saved cards for user %s: %v", userID, err) return } defer rows.Close() for rows.Next() { var cardID string if err := rows.Scan(&cardID); err != nil { log.Printf("Warning: Failed to scan card ID for user %s: %v", userID, err) continue } if err := payments.SquareClient.DeleteCardOnFile(context.Background(), cardID); err != nil { log.Printf("Warning: Failed to delete Square card %s for user %s: %v", cardID, userID, err) } } }() } // --- SQL-level anonymization/deletion --- if accountRole == "guest" { _, err = db.DB.Exec(ctx, `SELECT delete_guest_user($1)`, userID) if err != nil { log.Printf("Failed to delete guest user %s: %v", userID, err) http.Error(w, "server error", http.StatusInternalServerError) return } } else { _, err = db.DB.Exec(ctx, `SELECT anonymize_user($1)`, userID) if err != nil { log.Printf("Failed to anonymize user %s: %v", userID, err) http.Error(w, "server error", http.StatusInternalServerError) return } // TODO: Create 'user_anonymized' notification for admin audit trail } // Delete CardDAV contact (non-blocking, best-effort) if dav.Service != nil { go func() { uri := fmt.Sprintf("%s.vcf", userID) if err := dav.Service.DeleteContact(1, uri); err != nil { log.Printf("Warning: Failed to delete CardDAV contact for user %s: %v", userID, err) } }() } w.WriteHeader(http.StatusNoContent) }