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>
246 lines
7.5 KiB
Go
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
|
|
}
|