initial commit

This commit is contained in:
m.zare
2026-04-10 18:25:21 +03:30
commit 77ca6c34a3
263 changed files with 34470 additions and 0 deletions

View 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
}