211 lines
6.0 KiB
Go
211 lines
6.0 KiB
Go
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
|
|
}
|