- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
131 lines
3.7 KiB
Go
131 lines
3.7 KiB
Go
package handlers
|
|
|
|
// gdpr.go — v0.34.0 CS2
|
|
//
|
|
// GDPR delete endpoint: DELETE /api/v1/me
|
|
// Allows a user to delete their own account and all associated data.
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"switchboard-core/models"
|
|
"switchboard-core/store"
|
|
)
|
|
|
|
// GDPRHandler serves account deletion endpoints.
|
|
type GDPRHandler struct {
|
|
stores store.Stores
|
|
}
|
|
|
|
// NewGDPRHandler creates a new handler for GDPR operations.
|
|
func NewGDPRHandler(s store.Stores) *GDPRHandler {
|
|
return &GDPRHandler{stores: s}
|
|
}
|
|
|
|
// deleteAccountRequest is the body for DELETE /api/v1/me.
|
|
type deleteAccountRequest struct {
|
|
Confirm string `json:"confirm"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// DeleteMyAccount permanently deletes the requesting user's account.
|
|
// DELETE /api/v1/me
|
|
func (h *GDPRHandler) DeleteMyAccount(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
userID := c.GetString("user_id")
|
|
|
|
var req deleteAccountRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
// Require explicit confirmation
|
|
if req.Confirm != "DELETE" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "confirm field must be \"DELETE\""})
|
|
return
|
|
}
|
|
|
|
// Fetch user
|
|
user, err := h.stores.Users.GetByID(ctx, userID)
|
|
if err != nil || user == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
|
return
|
|
}
|
|
|
|
// Verify password
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid password"})
|
|
return
|
|
}
|
|
|
|
// Prevent deleting last admin
|
|
if user.Role == "admin" {
|
|
adminCount, err := h.stores.Export.CountActiveAdmins(ctx)
|
|
if err != nil {
|
|
slog.Error("gdpr: count admins", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check admin count"})
|
|
return
|
|
}
|
|
if adminCount <= 1 {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete the last admin account"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Generate anonymized hash from user ID
|
|
hash := sha256.Sum256([]byte(userID))
|
|
anonHash := fmt.Sprintf("%x", hash[:6]) // 12 hex chars
|
|
|
|
// Step 1: Soft-delete/hard-delete user data
|
|
counts, err := h.stores.Export.SoftDeleteUserData(ctx, userID)
|
|
if err != nil {
|
|
slog.Error("gdpr: delete user data", "error", err, "user_id", userID)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user data"})
|
|
return
|
|
}
|
|
|
|
// Step 2: Revoke tokens
|
|
if err := h.stores.Export.DeleteUserTokens(ctx, userID); err != nil {
|
|
slog.Error("gdpr: delete tokens", "error", err, "user_id", userID)
|
|
// Continue — tokens will expire naturally
|
|
}
|
|
|
|
// Step 3: Delete personal provider configs
|
|
if deleted, err := h.stores.Providers.DeletePersonalByOwner(ctx, userID); err != nil {
|
|
slog.Error("gdpr: delete provider configs", "error", err)
|
|
} else {
|
|
counts["provider_configs"] = deleted
|
|
}
|
|
|
|
// Step 4: Anonymize user record
|
|
if err := h.stores.Export.AnonymizeUser(ctx, userID, anonHash); err != nil {
|
|
slog.Error("gdpr: anonymize user", "error", err, "user_id", userID)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to anonymize user"})
|
|
return
|
|
}
|
|
|
|
// Audit log (before the user record is fully anonymized)
|
|
anonUsername := "deleted-user-" + anonHash
|
|
h.stores.Audit.Log(ctx, &models.AuditEntry{
|
|
ActorID: &userID,
|
|
Action: "user.gdpr_delete",
|
|
ResourceType: "user",
|
|
ResourceID: userID,
|
|
Metadata: models.JSONMap{"anonymized_as": anonUsername, "deleted_counts": counts},
|
|
})
|
|
|
|
slog.Info("gdpr: account deleted", "user_id", userID, "anonymized_as", anonUsername)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "account deleted",
|
|
"anonymized_as": anonUsername,
|
|
})
|
|
}
|