feat(user): extend DeleteAccountHandler with external system scrubbing
DeleteAccountHandler now scrubs external systems BEFORE SQL-level anonymization. S3: deletes profile picture from S3/R2 (profiles/{userID}.jpg) when s3.Client is configured. Square: iterates user's saved cards and calls DeleteCardOnFile for each non-deleted card when payments.SquareClient is configured. Both run in background goroutines (non-blocking, best-effort with warning logs). CardDAV contact deletion remains as existing best-effort goroutine.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/handlers/payments"
|
||||
"crussell/internal/dav"
|
||||
"crussell/internal/s3"
|
||||
"crussell/mw"
|
||||
)
|
||||
|
||||
@@ -20,26 +24,71 @@ func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var accountRole string
|
||||
err := db.DB.QueryRow(r.Context(), `SELECT account_role FROM users WHERE id = $1`, userID).Scan(&accountRole)
|
||||
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 err == sql.ErrNoRows {
|
||||
http.Error(w, "user not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Printf("Failed to fetch user role for deletion: %v", err)
|
||||
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(r.Context(), `SELECT delete_guest_user($1)`, userID)
|
||||
_, 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(r.Context(), `SELECT anonymize_user($1)`, userID)
|
||||
_, 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)
|
||||
|
||||
Reference in New Issue
Block a user