initial commit
This commit is contained in:
49
internal/application/auth/account_info.go
Normal file
49
internal/application/auth/account_info.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
func (s *service) GetUserInfo(ctx context.Context, userID uuid.UUID) (*dto.UserInfoResponse, error) {
|
||||
user, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
var profileID *uuid.UUID
|
||||
prof, err := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if err == nil && prof != nil {
|
||||
profileID = &prof.ID
|
||||
}
|
||||
|
||||
return &dto.UserInfoResponse{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Status: userStatusToString(user.Status),
|
||||
ProfileID: profileID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func userStatusToString(s auth.UserStatus) string {
|
||||
switch s {
|
||||
case auth.UserStatusActive:
|
||||
return "active"
|
||||
case auth.UserStatusInactive:
|
||||
return "inactive"
|
||||
case auth.UserStatusPending:
|
||||
return "pending"
|
||||
case auth.UserStatusDeleted:
|
||||
return "deleted"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
86
internal/application/auth/oauth.go
Normal file
86
internal/application/auth/oauth.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"base/pkg/jwt"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
func (s *service) GetOAuthRedirectURL(ctx context.Context, request dto.OAuthRedirectURLRequest) (string, error) {
|
||||
provider := s.oauthService.Client(request.Provider)
|
||||
|
||||
state := uuid.New().String()
|
||||
redirectURL := provider.GetConsentAuthUrl(ctx, state)
|
||||
|
||||
return redirectURL, nil
|
||||
}
|
||||
|
||||
func (s *service) OAuthCallback(ctx context.Context, request dto.OAuthCallbackRequest) (*dto.OAuthCallbackResponse, error) {
|
||||
oauthProvider := s.oauthService.Client(request.Provider)
|
||||
|
||||
token, err := oauthProvider.ExchangeCodeWithToken(ctx, request.Code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to exchange code for token: %w", err)
|
||||
}
|
||||
|
||||
userInfo, err := oauthProvider.GetUserInfo(ctx, token.AccessToken, token.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
accessExpiry := now.Add(time.Duration(token.ExpiresIn) * time.Second)
|
||||
refreshExpiry := now.Add(7 * 24 * time.Hour)
|
||||
|
||||
user := &auth.User{
|
||||
ID: uuid.New(), // Will be set by repository if user exists
|
||||
Email: userInfo.Email(),
|
||||
FirstName: userInfo.FirstName(),
|
||||
LastName: userInfo.LastName(),
|
||||
Status: auth.UserStatusActive,
|
||||
EmailVerified: true, // OAuth providers verify email
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
account := &auth.Account{
|
||||
ID: uuid.New(),
|
||||
Provider: request.Provider,
|
||||
AccessToken: &token.AccessToken,
|
||||
RefreshToken: &token.RefreshToken,
|
||||
AccessTokenExpiry: &accessExpiry,
|
||||
RefreshTokenExpiry: &refreshExpiry,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
isNewUser, err := s.userRepo.UpsertWithAccount(ctx, userInfo.Email(), user, account)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to upsert user and account")
|
||||
return nil, fmt.Errorf("failed to upsert user and account: %w", err)
|
||||
}
|
||||
|
||||
tokens, genErr := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()})
|
||||
if genErr != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", genErr)
|
||||
}
|
||||
|
||||
s.logger.Info().
|
||||
Str("user_id", user.ID.String()).
|
||||
Str("email", user.Email).
|
||||
Bool("is_new_user", isNewUser).
|
||||
Str("provider", request.Provider.String()).
|
||||
Msg("OAuth callback completed successfully")
|
||||
|
||||
return &dto.OAuthCallbackResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
IsNewUser: isNewUser,
|
||||
}, nil
|
||||
}
|
||||
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
|
||||
}
|
||||
131
internal/application/auth/reset_password.go
Normal file
131
internal/application/auth/reset_password.go
Normal file
@@ -0,0 +1,131 @@
|
||||
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/email"
|
||||
"base/pkg/hashids"
|
||||
"base/pkg/jwt"
|
||||
)
|
||||
|
||||
// SendResetPasswordEmail sends a password reset email
|
||||
func (s *service) SendResetPasswordEmail(ctx context.Context, request dto.SendResetPasswordEmailRequest) error {
|
||||
user, err := s.userRepo.FindByEmail(ctx, request.Email)
|
||||
if err != nil {
|
||||
// Don't reveal if user exists or not for security
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate reset code
|
||||
code := hashids.GenerateCode(int64(user.ID.Time()))
|
||||
key := fmt.Sprintf("reset_password:%s", user.ID.String())
|
||||
|
||||
// Store code in cache (15 minutes TTL)
|
||||
if storeErr := s.resetPasswordStore.Set(ctx, key, code, 15*time.Minute); storeErr != nil {
|
||||
return fmt.Errorf("failed to store reset password code: %w", storeErr)
|
||||
}
|
||||
|
||||
// Send email
|
||||
emailData := map[string]interface{}{
|
||||
"Code": code,
|
||||
"Name": user.FirstName,
|
||||
}
|
||||
|
||||
emailMsg := email.Request{
|
||||
To: user.Email,
|
||||
Subject: "Reset Your Password",
|
||||
Template: email.TemplateData{
|
||||
EmailTemplateName: email.TemplatePasswordReset,
|
||||
Data: emailData,
|
||||
},
|
||||
}
|
||||
|
||||
if _, sendEmailErr := s.emailService.Send(ctx, emailMsg); sendEmailErr != nil {
|
||||
return fmt.Errorf("failed to send reset password email: %w", sendEmailErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword resets a user's password with the provided code
|
||||
func (s *service) ResetPassword(ctx context.Context, request dto.ResetPasswordRequest) (*dto.TokenResponse, error) {
|
||||
user, err := s.userRepo.FindByEmail(ctx, request.Email, auth.WithAccounts())
|
||||
if err != nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
// Get code from cache
|
||||
key := fmt.Sprintf("reset_password:%s", user.ID.String())
|
||||
|
||||
storedCode, found, getErr := s.resetPasswordStore.Get(ctx, key)
|
||||
if getErr != nil || !found {
|
||||
return nil, ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
if storedCode != request.Code {
|
||||
return nil, ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
// Find credentials account
|
||||
var credentialsAccount *auth.Account
|
||||
for _, acc := range user.Accounts {
|
||||
if acc.Provider == oauth.Credentials {
|
||||
credentialsAccount = &acc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, genHashPassErr := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
|
||||
if genHashPassErr != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", genHashPassErr)
|
||||
}
|
||||
|
||||
hashedPasswordStr := string(hashedPassword)
|
||||
|
||||
if credentialsAccount != nil {
|
||||
// Update existing account
|
||||
credentialsAccount.Password = &hashedPasswordStr
|
||||
credentialsAccount.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.accountRepo.Update(ctx, credentialsAccount); err != nil {
|
||||
return nil, fmt.Errorf("failed to update account: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Create new credentials account
|
||||
account := &auth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Credentials,
|
||||
Password: &hashedPasswordStr,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.accountRepo.Create(ctx, account); err != nil {
|
||||
return nil, fmt.Errorf("failed to create account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete reset code from cache
|
||||
_ = s.resetPasswordStore.Delete(ctx, key)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
return &dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
87
internal/application/auth/service.go
Normal file
87
internal/application/auth/service.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/config"
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
"base/internal/pkg/oauth"
|
||||
"base/pkg/email"
|
||||
"base/pkg/jwt"
|
||||
"base/pkg/store"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
ErrInvalidVerificationCode = errors.New("invalid verification code")
|
||||
ErrEmailAlreadyVerified = errors.New("email already verified")
|
||||
ErrInvalidRefreshToken = errors.New("invalid refresh token")
|
||||
ErrAccountNotFound = errors.New("account not found")
|
||||
ErrProfileNotFound = errors.New("profile not found")
|
||||
ErrProfileAlreadyExists = errors.New("profile already exists")
|
||||
ErrHandleAlreadyTaken = errors.New("handle already taken")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
RegisterWithCredentials(ctx context.Context, request dto.RegisterRequest) (*dto.TokenResponse, error)
|
||||
LoginWithCredentials(ctx context.Context, email, password string) (*dto.TokenResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*dto.TokenResponse, error)
|
||||
GetOAuthRedirectURL(ctx context.Context, request dto.OAuthRedirectURLRequest) (string, error)
|
||||
OAuthCallback(ctx context.Context, request dto.OAuthCallbackRequest) (*dto.OAuthCallbackResponse, error)
|
||||
SendVerificationEmail(ctx context.Context, request dto.SendVerificationEmailRequest) error
|
||||
VerifyAccount(ctx context.Context, request dto.VerifyAccountRequest) error
|
||||
SendResetPasswordEmail(ctx context.Context, request dto.SendResetPasswordEmailRequest) error
|
||||
ResetPassword(ctx context.Context, request dto.ResetPasswordRequest) (*dto.TokenResponse, error)
|
||||
SetupProfile(ctx context.Context, userID uuid.UUID, request dto.SetupProfileRequest) error
|
||||
GetUserInfo(ctx context.Context, userID uuid.UUID) (*dto.UserInfoResponse, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
config *config.AppConfig
|
||||
userRepo auth.UserRepository
|
||||
accountRepo auth.AccountRepository
|
||||
profileRepo profile.Repository
|
||||
emailService email.Email
|
||||
oauthService oauth.OAuth
|
||||
verificationStore store.Store[string]
|
||||
resetPasswordStore store.Store[string]
|
||||
jwtService jwt.TokenService
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
Config *config.AppConfig
|
||||
UserRepo auth.UserRepository
|
||||
AccountRepo auth.AccountRepository
|
||||
ProfileRepo profile.Repository
|
||||
EmailService email.Email
|
||||
OAuthService oauth.OAuth
|
||||
VerificationStore store.Store[string] `name:"verification_store"`
|
||||
ResetPasswordStore store.Store[string] `name:"reset_password_store"`
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
config: param.Config,
|
||||
userRepo: param.UserRepo,
|
||||
accountRepo: param.AccountRepo,
|
||||
profileRepo: param.ProfileRepo,
|
||||
emailService: param.EmailService,
|
||||
oauthService: param.OAuthService,
|
||||
verificationStore: param.VerificationStore,
|
||||
resetPasswordStore: param.ResetPasswordStore,
|
||||
jwtService: jwt.New(param.Config.JWT.Secret, param.Config.JWT.AccessTokenExpiration, param.Config.JWT.RefreshTokenExpiration),
|
||||
}
|
||||
}
|
||||
76
internal/application/auth/setup_profile.go
Normal file
76
internal/application/auth/setup_profile.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
var slugRe = regexp.MustCompile(`[^a-z0-9-]+`)
|
||||
|
||||
func (s *service) SetupProfile(ctx context.Context, userID uuid.UUID, req dto.SetupProfileRequest) error {
|
||||
existingProfile, _ := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if existingProfile != nil {
|
||||
return ErrProfileAlreadyExists
|
||||
}
|
||||
|
||||
user, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
handle := generateHandle(user.FirstName, user.LastName, userID)
|
||||
if req.Handle != "" {
|
||||
handle = req.Handle
|
||||
}
|
||||
other, _ := s.profileRepo.FindByHandle(ctx, handle)
|
||||
if other != nil {
|
||||
return ErrHandleAlreadyTaken
|
||||
}
|
||||
|
||||
newProfile := &profile.Profile{
|
||||
ID: uuid.New(),
|
||||
UserID: &userID,
|
||||
Handle: handle,
|
||||
Hero: profile.Hero{
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
ShortDescription: req.ShortDescription,
|
||||
},
|
||||
Contact: profile.Contact{
|
||||
Email: user.Email,
|
||||
Phone: user.PhoneNumber,
|
||||
},
|
||||
PageSetting: profile.PageSetting{
|
||||
VisibilityLevel: "public",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
newProfile.Hero.Role = &profile.Role{ID: req.RoleID}
|
||||
|
||||
if req.RoleLevel != "" && newProfile.Hero.Role != nil {
|
||||
newProfile.Hero.Role.Level = req.RoleLevel
|
||||
} else if req.RoleLevel != "" {
|
||||
newProfile.Hero.Role = &profile.Role{Level: req.RoleLevel}
|
||||
}
|
||||
|
||||
return s.profileRepo.Create(ctx, newProfile)
|
||||
}
|
||||
|
||||
func generateHandle(firstName, lastName string, userID uuid.UUID) string {
|
||||
slug := slugRe.ReplaceAllString(strings.ToLower(strings.TrimSpace(firstName+"-"+lastName)), "-")
|
||||
slug = strings.Trim(slug, "-")
|
||||
if slug == "" {
|
||||
slug = "user"
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", slug, userID.String()[:8])
|
||||
}
|
||||
16
internal/application/auth/utils.go
Normal file
16
internal/application/auth/utils.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func generateOTP() (string, error) {
|
||||
newInt := big.NewInt(10000) // 0 .. 999999
|
||||
n, err := rand.Int(rand.Reader, newInt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%04d", n.Int64()), err
|
||||
}
|
||||
62
internal/application/auth/verify.go
Normal file
62
internal/application/auth/verify.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/dto"
|
||||
"base/pkg/email"
|
||||
)
|
||||
|
||||
func (s *service) SendVerificationEmail(ctx context.Context, request dto.SendVerificationEmailRequest) error {
|
||||
emailMsg := email.Request{
|
||||
To: request.Email,
|
||||
Subject: "Verify Your Email",
|
||||
Template: email.TemplateData{EmailTemplateName: email.TemplateEmailVerification},
|
||||
}
|
||||
|
||||
if _, err := s.emailService.Send(ctx, emailMsg); err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to send verification email")
|
||||
return fmt.Errorf("failed to send verification email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) VerifyAccount(ctx context.Context, request dto.VerifyAccountRequest) error {
|
||||
user, err := s.userRepo.FindByEmail(ctx, request.Email)
|
||||
if err != nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
return ErrEmailAlreadyVerified
|
||||
}
|
||||
|
||||
// Get code from cache
|
||||
key := fmt.Sprintf("verification:%s", request.Email)
|
||||
|
||||
storedCode, found, err := s.verificationStore.Get(ctx, key)
|
||||
if err != nil || !found {
|
||||
return ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
if storedCode != request.Code {
|
||||
return ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
user.EmailVerified = true
|
||||
user.Status = auth.UserStatusActive
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
// Delete verification code from cache
|
||||
_ = s.verificationStore.Delete(ctx, key)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user