package services import ( "context" "errors" "fmt" "github.com/rs/zerolog/log" "github.com/treytartt/honeydue-api/internal/apperrors" "github.com/treytartt/honeydue-api/internal/dto/requests" "github.com/treytartt/honeydue-api/internal/dto/responses" "github.com/treytartt/honeydue-api/internal/kratos" "github.com/treytartt/honeydue-api/internal/repositories" ) // AuthService handles user profile and account management. Session // authentication is now delegated to Ory Kratos via KratosAuth middleware. type AuthService struct { userRepo *repositories.UserRepository notificationRepo *repositories.NotificationRepository cache *CacheService kratos *kratos.Client } // NewAuthService creates a new auth service. func NewAuthService(userRepo *repositories.UserRepository, _ interface{}) *AuthService { return &AuthService{ userRepo: userRepo, } } // SetNotificationRepository wires the notification repo (kept for startup // compatibility — no longer used post-Kratos). func (s *AuthService) SetNotificationRepository(notificationRepo *repositories.NotificationRepository) { s.notificationRepo = notificationRepo } // SetCacheService wires Redis (kept for startup compatibility). func (s *AuthService) SetCacheService(cache *CacheService) { s.cache = cache } // SetKratosClient wires the Ory Kratos client so account deletion also // removes the user's Kratos identity. When nil (e.g. in tests) the Kratos // teardown is skipped. func (s *AuthService) SetKratosClient(k *kratos.Client) { s.kratos = k } // GetCurrentUser returns the current authenticated user with profile. func (s *AuthService) GetCurrentUser(ctx context.Context, userID uint) (*responses.CurrentUserResponse, error) { user, err := s.userRepo.WithContext(ctx).FindByIDWithProfile(userID) if err != nil { return nil, err } authProvider, err := s.userRepo.WithContext(ctx).FindAuthProvider(userID) if err != nil { log.Warn().Err(err).Uint("user_id", userID).Msg("Failed to determine auth provider") authProvider = "kratos" } response := responses.NewCurrentUserResponse(user, authProvider) return &response, nil } // UpdateProfile updates a user's profile fields. func (s *AuthService) UpdateProfile(ctx context.Context, userID uint, req *requests.UpdateProfileRequest) (*responses.CurrentUserResponse, error) { user, err := s.userRepo.WithContext(ctx).FindByID(userID) if err != nil { return nil, err } if req.Email != nil && *req.Email != user.Email { exists, err := s.userRepo.WithContext(ctx).ExistsByEmail(*req.Email) if err != nil { return nil, apperrors.Internal(err) } if exists { return nil, apperrors.Conflict("error.email_already_taken") } user.Email = *req.Email } if req.FirstName != nil { user.FirstName = *req.FirstName } if req.LastName != nil { user.LastName = *req.LastName } if err := s.userRepo.WithContext(ctx).Update(user); err != nil { return nil, apperrors.Internal(err) } user, err = s.userRepo.WithContext(ctx).FindByIDWithProfile(userID) if err != nil { return nil, err } authProvider, err := s.userRepo.WithContext(ctx).FindAuthProvider(userID) if err != nil { log.Warn().Err(err).Uint("user_id", userID).Msg("Failed to determine auth provider") authProvider = "kratos" } response := responses.NewCurrentUserResponse(user, authProvider) return &response, nil } // DeleteAccount deletes a user's account and all associated data. // Kratos owns credentials; confirmation string "DELETE" is required from the // caller since we can no longer verify a password here. // // The Kratos identity is deleted FIRST: Kratos owns the credentials, so if // that call fails the local data is left intact and an error is returned for // the caller to retry. Deleting local data first would risk an orphaned // Kratos identity that can still authenticate against a half-deleted account. func (s *AuthService) DeleteAccount(ctx context.Context, userID uint, _ *string, confirmation *string) ([]string, error) { if confirmation == nil || *confirmation != "DELETE" { return nil, apperrors.BadRequest("error.confirmation_required") } user, err := s.userRepo.WithContext(ctx).FindByID(userID) if err != nil { if errors.Is(err, repositories.ErrUserNotFound) { return nil, apperrors.NotFound("error.user_not_found") } return nil, apperrors.Internal(err) } // Tear down the Kratos identity before touching local data. DeleteIdentity // treats a 404 as success, so a retry after a partial failure is safe. if s.kratos != nil && user.KratosID != "" { if err := s.kratos.DeleteIdentity(ctx, user.KratosID); err != nil { return nil, apperrors.Internal(fmt.Errorf("delete kratos identity: %w", err)) } } var fileURLs []string txErr := s.userRepo.WithContext(ctx).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 }