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:
Trey T
2026-03-26 10:41:01 -05:00
parent 72866e935e
commit 4abc57535e
22 changed files with 1675 additions and 82 deletions

View File

@@ -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
}