package auth import ( "context" "fmt" "time" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "base/internal/domain/auth" "base/internal/dto" "base/internal/pkg/oauth" "base/pkg/jwt" ) func (s *service) RegisterWithCredentials(ctx context.Context, request dto.RegisterRequest) (*dto.TokenResponse, error) { // Check if user already exists existingUser, err := s.userRepo.FindByEmail(ctx, request.Email) if err == nil && existingUser != nil { return nil, ErrUserAlreadyExists } // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) if err != nil { s.logger.Error().Err(err).Msg("failed to hash password") return nil, fmt.Errorf("failed to hash password: %w", err) } hashedPasswordStr := string(hashedPassword) id, genErr := uuid.NewV7() if genErr != nil { return nil, genErr } // Create user and account within a transaction // If any operation fails, all changes are rolled back user := &auth.User{ ID: id, Email: request.Email, FirstName: request.FirstName, LastName: request.LastName, PhoneNumber: request.PhoneNumber, Status: auth.UserStatusPending, EmailVerified: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } account := &auth.Account{ ID: uuid.New(), UserID: user.ID, Provider: oauth.Credentials, Password: &hashedPasswordStr, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err = s.userRepo.CreateWithAccount(ctx, user, account); err != nil { s.logger.Error().Err(err).Msg("failed to create user and account") return nil, fmt.Errorf("failed to create user and account: %w", err) } // Generate tokens tokens, genTokenErr := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()}) if genTokenErr != nil { return nil, fmt.Errorf("failed to generate tokens: %w", genTokenErr) } // Update account with tokens account.AccessToken = &tokens.AccessToken account.RefreshToken = &tokens.RefreshToken now := time.Now() accessExpiry := now.Add(24 * time.Hour) refreshExpiry := now.Add(7 * 24 * time.Hour) account.AccessTokenExpiry = &accessExpiry account.RefreshTokenExpiry = &refreshExpiry if err = s.accountRepo.Update(ctx, account); err != nil { s.logger.Error().Err(err).Msg("failed to update account with tokens") // Don't fail the registration, tokens are already generated } // Profile is created when user calls setup-profile return &dto.TokenResponse{ AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, }, nil } func (s *service) LoginWithCredentials(ctx context.Context, email, password string) (*dto.TokenResponse, error) { // Find user by email with accounts user, err := s.userRepo.FindByEmail(ctx, email, auth.WithAccounts()) if err != nil { return nil, ErrInvalidCredentials } // Check user status if user.Status == auth.UserStatusDeleted { return nil, ErrInvalidCredentials } // Find credentials account var credentialsAccount *auth.Account for _, acc := range user.Accounts { if acc.Provider == oauth.Credentials { credentialsAccount = &acc break } } if credentialsAccount == nil || credentialsAccount.Password == nil { return nil, ErrInvalidCredentials } // Verify password if err = bcrypt.CompareHashAndPassword([]byte(*credentialsAccount.Password), []byte(password)); err != nil { return nil, ErrInvalidCredentials } // Generate tokens tokens, err := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()}) if err != nil { return nil, fmt.Errorf("failed to generate tokens: %w", err) } // Update account with tokens credentialsAccount.AccessToken = &tokens.AccessToken credentialsAccount.RefreshToken = &tokens.RefreshToken now := time.Now() accessExpiry := now.Add(24 * time.Hour) refreshExpiry := now.Add(7 * 24 * time.Hour) credentialsAccount.AccessTokenExpiry = &accessExpiry credentialsAccount.RefreshTokenExpiry = &refreshExpiry if err := s.accountRepo.Update(ctx, credentialsAccount); err != nil { s.logger.Error().Err(err).Msg("failed to update account with tokens") // Don't fail the login, tokens are already generated } return &dto.TokenResponse{ AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, }, nil } func (s *service) RefreshToken(ctx context.Context, refreshToken string) (*dto.TokenResponse, error) { claims, err := s.jwtService.VerifyToken(ctx, refreshToken) if err != nil { return nil, ErrInvalidRefreshToken } userID, err := uuid.Parse(claims.Sub) if err != nil { return nil, ErrInvalidRefreshToken } // Find user user, err := s.userRepo.FindByID(ctx, userID) if err != nil { return nil, ErrUserNotFound } accounts, err := s.accountRepo.FindByUserID(ctx, userID) if err != nil { return nil, ErrAccountNotFound } var matchingAccount *auth.Account for _, acc := range accounts { if acc.RefreshToken != nil && *acc.RefreshToken == refreshToken { // Check if refresh token is expired if acc.RefreshTokenExpiry != nil && acc.RefreshTokenExpiry.Before(time.Now()) { return nil, ErrInvalidRefreshToken } matchingAccount = acc break } } if matchingAccount == nil { return nil, ErrInvalidRefreshToken } tokens, err := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()}) if err != nil { return nil, fmt.Errorf("failed to generate tokens: %w", err) } matchingAccount.AccessToken = &tokens.AccessToken matchingAccount.RefreshToken = &tokens.RefreshToken now := time.Now() accessExpiry := now.Add(24 * time.Hour) refreshExpiry := now.Add(7 * 24 * time.Hour) matchingAccount.AccessTokenExpiry = &accessExpiry matchingAccount.RefreshTokenExpiry = &refreshExpiry if err = s.accountRepo.Update(ctx, matchingAccount); err != nil { s.logger.Error().Err(err).Msg("failed to update account with tokens") // Don't fail the refresh, tokens are already generated } return &dto.TokenResponse{ AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, }, nil }