initial commit
This commit is contained in:
210
internal/application/auth/register.go
Normal file
210
internal/application/auth/register.go
Normal file
@@ -0,0 +1,210 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user