Files
apskel-pos-backend/internal/validator/customer_auth_validator.go
efrilmandClaude Opus 5.5 8bf2fe5585 fix(customer-auth): make organization_id optional at registration
Requiring organization_id broke the current app, which does not send it.
When it is left out and the database has exactly one organization, the
customer now joins that one, so the app works unchanged. A sent
organization_id must still exist, and with several organizations and none
sent registration is refused with a clear message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:16:31 +07:00

246 lines
7.5 KiB
Go

package validator
import (
"errors"
"regexp"
"strings"
"github.com/google/uuid"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
)
type CustomerAuthValidator interface {
ValidateCheckPhoneRequest(req *contract.CheckPhoneRequest) (error, string)
ValidateRegisterStartRequest(req *contract.RegisterStartRequest) (error, string)
ValidateRegisterVerifyOtpRequest(req *contract.RegisterVerifyOtpRequest) (error, string)
ValidateRegisterSetPasswordRequest(req *contract.RegisterSetPasswordRequest) (error, string)
ValidateCustomerLoginRequest(req *contract.CustomerLoginRequest) (error, string)
ValidateResendOtpRequest(req *contract.ResendOtpRequest) (error, string)
}
type CustomerAuthValidatorImpl struct{}
func NewCustomerAuthValidator() CustomerAuthValidator {
return &CustomerAuthValidatorImpl{}
}
func (v *CustomerAuthValidatorImpl) ValidateCheckPhoneRequest(req *contract.CheckPhoneRequest) (error, string) {
if req == nil {
return errors.New("request is required"), constants.ValidationErrorCode
}
// Validate phone number
if strings.TrimSpace(req.PhoneNumber) == "" {
return errors.New("phone number is required"), constants.ValidationErrorCode
}
if !v.isValidPhoneNumber(req.PhoneNumber) {
return errors.New("invalid phone number format"), constants.ValidationErrorCode
}
return nil, ""
}
func (v *CustomerAuthValidatorImpl) ValidateRegisterStartRequest(req *contract.RegisterStartRequest) (error, string) {
if req == nil {
return errors.New("request is required"), constants.ValidationErrorCode
}
// Validate phone number
if strings.TrimSpace(req.PhoneNumber) == "" {
return errors.New("phone number is required"), constants.ValidationErrorCode
}
if !v.isValidPhoneNumber(req.PhoneNumber) {
return errors.New("invalid phone number format"), constants.ValidationErrorCode
}
// Validate name
if strings.TrimSpace(req.Name) == "" {
return errors.New("name is required"), constants.ValidationErrorCode
}
if len(req.Name) < 2 {
return errors.New("name must be at least 2 characters long"), constants.ValidationErrorCode
}
if len(req.Name) > 100 {
return errors.New("name cannot exceed 100 characters"), constants.ValidationErrorCode
}
// Validate organization
if orgID := strings.TrimSpace(req.OrganizationID); orgID != "" {
if _, err := uuid.Parse(orgID); err != nil {
return errors.New("organization_id must be a valid UUID"), constants.ValidationErrorCode
}
}
// Validate birth date
if strings.TrimSpace(req.BirthDate) == "" {
return errors.New("birth date is required"), constants.ValidationErrorCode
}
if !v.isValidDateFormat(req.BirthDate) {
return errors.New("invalid birth date format (YYYY-MM-DD)"), constants.ValidationErrorCode
}
return nil, ""
}
func (v *CustomerAuthValidatorImpl) ValidateRegisterVerifyOtpRequest(req *contract.RegisterVerifyOtpRequest) (error, string) {
if req == nil {
return errors.New("request is required"), constants.ValidationErrorCode
}
// Validate registration token
if strings.TrimSpace(req.RegistrationToken) == "" {
return errors.New("registration token is required"), constants.ValidationErrorCode
}
// Validate OTP code
if strings.TrimSpace(req.OtpCode) == "" {
return errors.New("OTP code is required"), constants.ValidationErrorCode
}
if !v.isValidOtpCode(req.OtpCode) {
return errors.New("invalid OTP code format"), constants.ValidationErrorCode
}
return nil, ""
}
func (v *CustomerAuthValidatorImpl) ValidateRegisterSetPasswordRequest(req *contract.RegisterSetPasswordRequest) (error, string) {
if req == nil {
return errors.New("request is required"), constants.ValidationErrorCode
}
// Validate registration token
if strings.TrimSpace(req.RegistrationToken) == "" {
return errors.New("registration token is required"), constants.ValidationErrorCode
}
// Validate password
if strings.TrimSpace(req.Password) == "" {
return errors.New("password is required"), constants.ValidationErrorCode
}
if len(req.Password) < 8 {
return errors.New("password must be at least 8 characters long"), constants.ValidationErrorCode
}
if len(req.Password) > 128 {
return errors.New("password cannot exceed 128 characters"), constants.ValidationErrorCode
}
// Validate confirm password
if strings.TrimSpace(req.ConfirmPassword) == "" {
return errors.New("confirm password is required"), constants.ValidationErrorCode
}
if req.Password != req.ConfirmPassword {
return errors.New("passwords do not match"), constants.ValidationErrorCode
}
// Validate password strength
if !v.isStrongPassword(req.Password) {
return errors.New("password must contain at least one uppercase letter, one lowercase letter, and one number"), constants.ValidationErrorCode
}
return nil, ""
}
func (v *CustomerAuthValidatorImpl) ValidateCustomerLoginRequest(req *contract.CustomerLoginRequest) (error, string) {
if req == nil {
return errors.New("request is required"), constants.ValidationErrorCode
}
// Validate phone number
if strings.TrimSpace(req.PhoneNumber) == "" {
return errors.New("phone number is required"), constants.ValidationErrorCode
}
if !v.isValidPhoneNumber(req.PhoneNumber) {
return errors.New("invalid phone number format"), constants.ValidationErrorCode
}
// Validate password
if strings.TrimSpace(req.Password) == "" {
return errors.New("password is required"), constants.ValidationErrorCode
}
return nil, ""
}
// Helper validation functions
func (v *CustomerAuthValidatorImpl) isValidPhoneNumber(phoneNumber string) bool {
// Basic phone number validation - adjust regex based on your requirements
phoneRegex := regexp.MustCompile(`^\+?[1-9]\d{1,14}$`)
return phoneRegex.MatchString(phoneNumber)
}
func (v *CustomerAuthValidatorImpl) isValidDateFormat(date string) bool {
// Basic date format validation for YYYY-MM-DD
dateRegex := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
if !dateRegex.MatchString(date) {
return false
}
// You can add more sophisticated date validation here if needed
return true
}
func (v *CustomerAuthValidatorImpl) isValidOtpCode(code string) bool {
// OTP code should be 4-8 digits
otpRegex := regexp.MustCompile(`^\d{4,8}$`)
return otpRegex.MatchString(code)
}
func (v *CustomerAuthValidatorImpl) ValidateResendOtpRequest(req *contract.ResendOtpRequest) (error, string) {
if req == nil {
return errors.New("request is required"), constants.CustomerEntity
}
// Validate phone number
if req.PhoneNumber == "" {
return errors.New("phone number is required"), constants.CustomerEntity
}
// Validate phone number format
if !v.isValidPhoneNumber(req.PhoneNumber) {
return errors.New("invalid phone number format"), constants.CustomerEntity
}
// Validate purpose
if req.Purpose == "" {
return errors.New("purpose is required"), constants.CustomerEntity
}
// Validate purpose values
validPurposes := []string{"login", "registration"}
if !v.contains(validPurposes, req.Purpose) {
return errors.New("purpose must be either 'login' or 'registration'"), constants.CustomerEntity
}
return nil, ""
}
func (v *CustomerAuthValidatorImpl) isStrongPassword(password string) bool {
// Password must contain at least one uppercase, one lowercase, and one number
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
return hasUpper && hasLower && hasNumber
}
func (v *CustomerAuthValidatorImpl) contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}