Files
honeyDueAPI/internal/services/auth_service.go
T
Trey t 81578f6e27
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
feat(auth): replace hand-rolled auth with Ory Kratos — phase 2 backend
Delegates all credential management (login, register, password reset,
email verification, social sign-in) to Ory Kratos. The Go API now acts
as a resource server: the new KratosAuth middleware validates sessions
against the Kratos whoami endpoint, writes the local User mirror into
Echo context, and all existing domain handlers continue working
unchanged. Hand-rolled token auth, AuthToken model, apple_auth/
google_auth services, and the auth refresh flow are removed. Tests are
updated to use the fake-token middleware pattern so existing integration
assertions require no rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:55:56 -05:00

133 lines
4.0 KiB
Go

package services
import (
"context"
"errors"
"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/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
}
// 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
}
// 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.
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")
}
_, 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)
}
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
}