Add delete account endpoint and file encryption at rest
Delete Account (Plan #2): - DELETE /api/auth/account/ with password or "DELETE" confirmation - Cascade delete across 15+ tables in correct FK order - Auth provider detection (email/apple/google) for /auth/me/ - File cleanup after account deletion - Handler + repository tests (12 tests) Encryption at Rest (Plan #3): - AES-256-GCM envelope encryption (per-file DEK wrapped by KEK) - Encrypt on upload, auto-decrypt on serve via StorageService.ReadFile() - MediaHandler serves decrypted files via c.Blob() - TaskService email image loading uses ReadFile() - cmd/migrate-encrypt CLI tool with --dry-run for existing files - Encryption service + storage service tests (18 tests)
This commit is contained in:
@@ -200,10 +200,69 @@ func (s *AuthService) GetCurrentUser(userID uint) (*responses.CurrentUserRespons
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := responses.NewCurrentUserResponse(user)
|
||||
authProvider, err := s.userRepo.FindAuthProvider(userID)
|
||||
if err != nil {
|
||||
// Log but don't fail - default to "email"
|
||||
log.Warn().Err(err).Uint("user_id", userID).Msg("Failed to determine auth provider")
|
||||
authProvider = "email"
|
||||
}
|
||||
|
||||
response := responses.NewCurrentUserResponse(user, authProvider)
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// DeleteAccount deletes a user's account and all associated data.
|
||||
// For email auth users, password verification is required.
|
||||
// For social auth users, confirmation string "DELETE" is required.
|
||||
// Returns a list of file URLs that need to be deleted from disk.
|
||||
func (s *AuthService) DeleteAccount(userID uint, password, confirmation *string) ([]string, error) {
|
||||
// Fetch user
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repositories.ErrUserNotFound) {
|
||||
return nil, apperrors.NotFound("error.user_not_found")
|
||||
}
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Determine auth provider
|
||||
authProvider, err := s.userRepo.FindAuthProvider(userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Validate credentials based on auth provider
|
||||
if authProvider == "email" {
|
||||
if password == nil || *password == "" {
|
||||
return nil, apperrors.BadRequest("error.password_required")
|
||||
}
|
||||
if !user.CheckPassword(*password) {
|
||||
return nil, apperrors.Unauthorized("error.invalid_credentials")
|
||||
}
|
||||
} else {
|
||||
// Social auth (apple or google) - require confirmation
|
||||
if confirmation == nil || *confirmation != "DELETE" {
|
||||
return nil, apperrors.BadRequest("error.confirmation_required")
|
||||
}
|
||||
}
|
||||
|
||||
// Start transaction and cascade delete
|
||||
var fileURLs []string
|
||||
txErr := s.userRepo.Transaction(func(txRepo *repositories.UserRepository) error {
|
||||
urls, err := txRepo.DeleteUserCascade(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileURLs = urls
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return nil, apperrors.Internal(txErr)
|
||||
}
|
||||
|
||||
return fileURLs, nil
|
||||
}
|
||||
|
||||
// UpdateProfile updates a user's profile
|
||||
func (s *AuthService) UpdateProfile(userID uint, req *requests.UpdateProfileRequest) (*responses.CurrentUserResponse, error) {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
@@ -240,7 +299,13 @@ func (s *AuthService) UpdateProfile(userID uint, req *requests.UpdateProfileRequ
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := responses.NewCurrentUserResponse(user)
|
||||
authProvider, err := s.userRepo.FindAuthProvider(userID)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Uint("user_id", userID).Msg("Failed to determine auth provider")
|
||||
authProvider = "email"
|
||||
}
|
||||
|
||||
response := responses.NewCurrentUserResponse(user, authProvider)
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user