441 lines
14 KiB
Go
441 lines
14 KiB
Go
package processor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"apskel-pos-be/internal/contract"
|
|
"apskel-pos-be/internal/entities"
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/repository"
|
|
"apskel-pos-be/internal/util"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type CustomerAuthProcessor interface {
|
|
CheckPhoneNumber(ctx context.Context, req *contract.CheckPhoneRequest) (*models.CheckPhoneResponse, error)
|
|
StartRegistration(ctx context.Context, req *contract.RegisterStartRequest) (*models.RegisterStartResponse, error)
|
|
VerifyOtp(ctx context.Context, req *contract.RegisterVerifyOtpRequest) (*models.RegisterVerifyOtpResponse, error)
|
|
SetPassword(ctx context.Context, req *contract.RegisterSetPasswordRequest) (*models.RegisterSetPasswordResponse, error)
|
|
Login(ctx context.Context, req *contract.CustomerLoginRequest) (*models.CustomerLoginResponse, error)
|
|
ResendOtp(ctx context.Context, req *contract.ResendOtpRequest) (*models.ResendOtpResponse, error)
|
|
}
|
|
|
|
type customerAuthProcessor struct {
|
|
customerAuthRepo repository.CustomerAuthRepository
|
|
otpProcessor OtpProcessor
|
|
otpRepo repository.OtpRepository
|
|
jwtSecret string
|
|
tokenTTLMinutes int
|
|
}
|
|
|
|
func NewCustomerAuthProcessor(customerAuthRepo repository.CustomerAuthRepository, otpProcessor OtpProcessor, otpRepo repository.OtpRepository, jwtSecret string, tokenTTLMinutes int) CustomerAuthProcessor {
|
|
return &customerAuthProcessor{
|
|
customerAuthRepo: customerAuthRepo,
|
|
otpProcessor: otpProcessor,
|
|
otpRepo: otpRepo,
|
|
jwtSecret: jwtSecret,
|
|
tokenTTLMinutes: tokenTTLMinutes,
|
|
}
|
|
}
|
|
|
|
func (p *customerAuthProcessor) CheckPhoneNumber(ctx context.Context, req *contract.CheckPhoneRequest) (*models.CheckPhoneResponse, error) {
|
|
// Check if phone number exists in database
|
|
exists, err := p.customerAuthRepo.CheckPhoneNumberExists(ctx, req.PhoneNumber)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check phone number: %w", err)
|
|
}
|
|
|
|
if !exists {
|
|
// Phone number not registered
|
|
return &models.CheckPhoneResponse{
|
|
Status: "NOT_REGISTERED",
|
|
Message: "Phone number not registered. Please continue registration.",
|
|
Data: &models.CheckPhoneResponseData{
|
|
PhoneNumber: req.PhoneNumber,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Phone number exists, get customer details
|
|
customer, err := p.customerAuthRepo.GetCustomerByPhoneNumber(ctx, req.PhoneNumber)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get customer: %w", err)
|
|
}
|
|
|
|
if customer == nil {
|
|
return nil, fmt.Errorf("customer not found")
|
|
}
|
|
|
|
// Check if customer has password set
|
|
if customer.PasswordHash == nil || *customer.PasswordHash == "" {
|
|
// Customer exists but no password set, send OTP for password setup
|
|
otpSession, err := p.otpProcessor.CreateOtpSession(ctx, req.PhoneNumber, "password_setup")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create OTP session: %w", err)
|
|
}
|
|
|
|
// Send OTP via WhatsApp
|
|
if err := p.otpProcessor.SendOtpViaWhatsApp(req.PhoneNumber, otpSession.Code, "password setup"); err != nil {
|
|
return nil, fmt.Errorf("failed to send OTP: %w", err)
|
|
}
|
|
|
|
return &models.CheckPhoneResponse{
|
|
Status: "OTP_REQUIRED",
|
|
Message: "OTP sent for password setup.",
|
|
Data: &models.CheckPhoneResponseData{
|
|
OtpToken: otpSession.Token,
|
|
ExpiresIn: 300,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Customer exists and has password set, validate password if provided
|
|
if req.Password == "" {
|
|
return &models.CheckPhoneResponse{
|
|
Status: "PASSWORD_REQUIRED",
|
|
Message: "Password is required for login.",
|
|
}, nil
|
|
}
|
|
|
|
// Validate password
|
|
if err := bcrypt.CompareHashAndPassword([]byte(*customer.PasswordHash), []byte(req.Password)); err != nil {
|
|
return &models.CheckPhoneResponse{
|
|
Status: "INVALID_PASSWORD",
|
|
Message: "Invalid password.",
|
|
}, nil
|
|
}
|
|
|
|
// Generate JWT tokens using customer JWT util
|
|
accessToken, refreshToken, _, err := util.GenerateCustomerTokens(customer, p.jwtSecret, p.tokenTTLMinutes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
|
}
|
|
|
|
return &models.CheckPhoneResponse{
|
|
Status: "SUCCESS",
|
|
Message: "Login successful.",
|
|
Data: &models.CheckPhoneResponseData{
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
User: &models.CustomerUserData{
|
|
ID: customer.ID,
|
|
Name: customer.Name,
|
|
PhoneNumber: *customer.PhoneNumber,
|
|
BirthDate: customer.BirthDate.Format("2006-01-02"),
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *customerAuthProcessor) StartRegistration(ctx context.Context, req *contract.RegisterStartRequest) (*models.RegisterStartResponse, error) {
|
|
// Check if phone number already exists
|
|
exists, err := p.customerAuthRepo.CheckPhoneNumberExists(ctx, req.PhoneNumber)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check phone number: %w", err)
|
|
}
|
|
|
|
if exists {
|
|
return nil, fmt.Errorf("phone number already registered")
|
|
}
|
|
|
|
// Generate registration token and create OTP session
|
|
registrationToken := uuid.New().String()
|
|
|
|
// Create OTP session for registration
|
|
otpSession, err := p.otpProcessor.CreateOtpSession(ctx, req.PhoneNumber, "registration")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create OTP session: %w", err)
|
|
}
|
|
|
|
// Store registration data in OTP session metadata
|
|
registrationData := map[string]interface{}{
|
|
"registration_token": registrationToken,
|
|
"name": req.Name,
|
|
"birth_date": req.BirthDate,
|
|
"step": "otp_sent",
|
|
}
|
|
|
|
// Update OTP session with registration metadata
|
|
otpSession.Metadata = registrationData
|
|
if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil {
|
|
return nil, fmt.Errorf("failed to update OTP session with registration data: %w", err)
|
|
}
|
|
|
|
// Send OTP via WhatsApp
|
|
if err := p.otpProcessor.SendOtpViaWhatsApp(req.PhoneNumber, otpSession.Code, "registration"); err != nil {
|
|
return nil, fmt.Errorf("failed to send OTP: %w", err)
|
|
}
|
|
|
|
return &models.RegisterStartResponse{
|
|
Status: "PENDING_OTP",
|
|
Message: "OTP sent to phone number for verification.",
|
|
Data: &models.RegisterStartResponseData{
|
|
RegistrationToken: registrationToken,
|
|
OtpToken: otpSession.Token,
|
|
ExpiresIn: 300,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *customerAuthProcessor) VerifyOtp(ctx context.Context, req *contract.RegisterVerifyOtpRequest) (*models.RegisterVerifyOtpResponse, error) {
|
|
otpSession, err := p.otpRepo.GetOtpSessionByRegistrationToken(ctx, req.RegistrationToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get OTP session: %w", err)
|
|
}
|
|
|
|
if otpSession == nil {
|
|
return nil, fmt.Errorf("invalid or expired registration token")
|
|
}
|
|
|
|
if otpSession.IsExpired() {
|
|
return nil, fmt.Errorf("registration token expired")
|
|
}
|
|
|
|
if !p.otpProcessor.ValidateOtpCode(req.OtpCode) {
|
|
return &models.RegisterVerifyOtpResponse{
|
|
Status: "FAILED",
|
|
Message: "Invalid OTP format.",
|
|
}, nil
|
|
}
|
|
|
|
if otpSession.Code != req.OtpCode {
|
|
otpSession.IncrementAttempts()
|
|
if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil {
|
|
fmt.Printf("Warning: failed to update OTP session attempts: %v\n", err)
|
|
}
|
|
return &models.RegisterVerifyOtpResponse{
|
|
Status: "FAILED",
|
|
Message: "Invalid OTP code.",
|
|
}, nil
|
|
}
|
|
|
|
if otpSession.IsUsed || otpSession.IsMaxAttemptsReached() {
|
|
return &models.RegisterVerifyOtpResponse{
|
|
Status: "FAILED",
|
|
Message: "OTP code already used or max attempts reached.",
|
|
}, nil
|
|
}
|
|
|
|
// Mark OTP as used
|
|
otpSession.MarkAsUsed()
|
|
|
|
// Update registration step in metadata
|
|
if otpSession.Metadata == nil {
|
|
otpSession.Metadata = make(map[string]interface{})
|
|
}
|
|
otpSession.Metadata["step"] = "otp_verified"
|
|
|
|
if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil {
|
|
return &models.RegisterVerifyOtpResponse{
|
|
Status: "FAILED",
|
|
Message: "Failed to update OTP session.",
|
|
}, nil
|
|
}
|
|
|
|
return &models.RegisterVerifyOtpResponse{
|
|
Status: "OTP_VERIFIED",
|
|
Message: "OTP verified, continue to set password.",
|
|
Data: &models.RegisterVerifyOtpResponseData{
|
|
RegistrationToken: req.RegistrationToken,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *customerAuthProcessor) SetPassword(ctx context.Context, req *contract.RegisterSetPasswordRequest) (*models.RegisterSetPasswordResponse, error) {
|
|
if req.Password != req.ConfirmPassword {
|
|
return nil, fmt.Errorf("passwords do not match")
|
|
}
|
|
|
|
// Get OTP session by registration token from metadata
|
|
otpSession, err := p.otpRepo.GetOtpSessionByRegistrationToken(ctx, req.RegistrationToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get OTP session: %w", err)
|
|
}
|
|
|
|
if otpSession == nil {
|
|
return nil, fmt.Errorf("invalid or expired registration token")
|
|
}
|
|
|
|
if otpSession.IsExpired() {
|
|
return nil, fmt.Errorf("registration token expired")
|
|
}
|
|
|
|
step, ok := otpSession.Metadata["step"].(string)
|
|
if !ok || step != "otp_verified" {
|
|
return nil, fmt.Errorf("OTP verification required before setting password")
|
|
}
|
|
|
|
// Hash password
|
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
|
|
passwordHashStr := string(passwordHash)
|
|
|
|
// Extract registration data from OTP session metadata
|
|
name, ok := otpSession.Metadata["name"].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid registration data: name not found")
|
|
}
|
|
|
|
birthDateStr, ok := otpSession.Metadata["birth_date"].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid registration data: birth_date not found")
|
|
}
|
|
|
|
// Parse birth date
|
|
birthDate, err := time.Parse("2006-01-02", birthDateStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid birth date format: %w", err)
|
|
}
|
|
|
|
defaultOrgID := uuid.MustParse("87bec7c1-e274-4f66-bac5-84e632208470") // This should be configurable
|
|
|
|
customer := &entities.Customer{
|
|
OrganizationID: defaultOrgID,
|
|
Name: name,
|
|
PhoneNumber: &otpSession.PhoneNumber,
|
|
BirthDate: &birthDate,
|
|
PasswordHash: &passwordHashStr,
|
|
IsActive: true,
|
|
}
|
|
|
|
if err := p.customerAuthRepo.CreateCustomer(ctx, customer); err != nil {
|
|
return nil, fmt.Errorf("failed to create customer: %w", err)
|
|
}
|
|
|
|
accessToken, refreshToken, _, err := util.GenerateCustomerTokens(customer, p.jwtSecret, p.tokenTTLMinutes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
|
}
|
|
|
|
return &models.RegisterSetPasswordResponse{
|
|
Status: "REGISTERED",
|
|
Message: "Registration completed successfully.",
|
|
Data: &models.RegisterSetPasswordResponseData{
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
User: &models.CustomerUserData{
|
|
ID: customer.ID,
|
|
Name: customer.Name,
|
|
PhoneNumber: *customer.PhoneNumber,
|
|
BirthDate: birthDate.Format("2006-01-02"),
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *customerAuthProcessor) Login(ctx context.Context, req *contract.CustomerLoginRequest) (*models.CustomerLoginResponse, error) {
|
|
// Get customer by phone number
|
|
customer, err := p.customerAuthRepo.GetCustomerByPhoneNumber(ctx, req.PhoneNumber)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get customer: %w", err)
|
|
}
|
|
|
|
if customer == nil {
|
|
return nil, fmt.Errorf("customer not found")
|
|
}
|
|
|
|
if customer.PasswordHash == nil {
|
|
return nil, fmt.Errorf("customer not properly registered")
|
|
}
|
|
|
|
// Verify password
|
|
if err := bcrypt.CompareHashAndPassword([]byte(*customer.PasswordHash), []byte(req.Password)); err != nil {
|
|
return nil, fmt.Errorf("invalid password")
|
|
}
|
|
|
|
// Generate JWT tokens using customer JWT util
|
|
accessToken, refreshToken, _, err := util.GenerateCustomerTokens(customer, p.jwtSecret, p.tokenTTLMinutes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
|
}
|
|
|
|
return &models.CustomerLoginResponse{
|
|
Status: "SUCCESS",
|
|
Message: "Login successful.",
|
|
Data: &models.CustomerLoginResponseData{
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
User: &models.CustomerUserData{
|
|
ID: customer.ID,
|
|
Name: customer.Name,
|
|
PhoneNumber: *customer.PhoneNumber,
|
|
BirthDate: customer.BirthDate.Format("2006-01-02"),
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *customerAuthProcessor) ResendOtp(ctx context.Context, req *contract.ResendOtpRequest) (*models.ResendOtpResponse, error) {
|
|
// Check if resend is allowed
|
|
canResend, secondsUntilNext, err := p.otpProcessor.CanResendOtp(ctx, req.PhoneNumber, req.Purpose)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check resend eligibility: %w", err)
|
|
}
|
|
|
|
if !canResend {
|
|
return &models.ResendOtpResponse{
|
|
Status: "RESEND_NOT_ALLOWED",
|
|
Message: fmt.Sprintf("Please wait %d seconds before requesting a new OTP", secondsUntilNext),
|
|
Data: &models.ResendOtpResponseData{
|
|
NextResendIn: secondsUntilNext,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// For registration purpose, check if phone number is already registered
|
|
if req.Purpose == "registration" {
|
|
exists, err := p.customerAuthRepo.CheckPhoneNumberExists(ctx, req.PhoneNumber)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check phone number: %w", err)
|
|
}
|
|
if exists {
|
|
return &models.ResendOtpResponse{
|
|
Status: "PHONE_ALREADY_REGISTERED",
|
|
Message: "Phone number is already registered. Please use login instead.",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// For login purpose, check if phone number is registered
|
|
if req.Purpose == "login" {
|
|
exists, err := p.customerAuthRepo.CheckPhoneNumberExists(ctx, req.PhoneNumber)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check phone number: %w", err)
|
|
}
|
|
if !exists {
|
|
return &models.ResendOtpResponse{
|
|
Status: "PHONE_NOT_REGISTERED",
|
|
Message: "Phone number is not registered. Please register first.",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// Resend OTP
|
|
otpSession, err := p.otpProcessor.ResendOtpSession(ctx, req.PhoneNumber, req.Purpose)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resend OTP: %w", err)
|
|
}
|
|
|
|
// Calculate next resend time (60 seconds from now)
|
|
nextResendIn := 60
|
|
|
|
return &models.ResendOtpResponse{
|
|
Status: "SUCCESS",
|
|
Message: "OTP resent successfully.",
|
|
Data: &models.ResendOtpResponseData{
|
|
OtpToken: otpSession.Token,
|
|
ExpiresIn: 300, // 5 minutes
|
|
NextResendIn: nextResendIn,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Helper functions - OTP generation is now handled by OtpProcessor
|