Main #41

Merged
aefril merged 16 commits from main into staging 2026-10-08 04:32:27 +02:00
4 changed files with 55 additions and 14 deletions
Showing only changes of commit 8bf2fe5585 - Show all commits
+3 -2
View File
@@ -17,8 +17,9 @@ type RegisterStartRequest struct {
Name string `json:"name" binding:"required"`
BirthDate string `json:"birth_date" binding:"required"`
// The organization (brand) the customer registers with. A customer belongs to one
// organization; the app sends the one it is built for.
OrganizationID string `json:"organization_id" binding:"required"`
// organization. Optional: when it is left out and the database has exactly one
// organization, the customer joins that one.
OrganizationID string `json:"organization_id,omitempty"`
}
type RegisterVerifyOtpRequest struct {
+37 -10
View File
@@ -143,19 +143,12 @@ func (p *customerAuthProcessor) StartRegistration(ctx context.Context, req *cont
return nil, fmt.Errorf("phone number already registered")
}
// The customer joins the organization the app is built for. Check it exists now,
// before an OTP is sent, rather than failing on a foreign key at the last step.
organizationID, err := uuid.Parse(strings.TrimSpace(req.OrganizationID))
if err != nil {
return nil, fmt.Errorf("organization_id must be a valid UUID")
}
orgExists, err := p.customerAuthRepo.OrganizationExists(ctx, organizationID)
// Resolve the organization before an OTP is sent, rather than failing on a foreign
// key at the last step.
organizationID, err := p.registrationOrganization(ctx, req.OrganizationID)
if err != nil {
return nil, err
}
if !orgExists {
return nil, fmt.Errorf("organization not found")
}
// Generate registration token and create OTP session
registrationToken := uuid.New().String()
@@ -458,3 +451,37 @@ func (p *customerAuthProcessor) ResendOtp(ctx context.Context, req *contract.Res
}
// Helper functions - OTP generation is now handled by OtpProcessor
// registrationOrganization is the organization a new customer joins: the one the app
// sent, which must exist, or, when the app sent none, the only organization there is.
// With several organizations and none sent there is no way to choose, so it refuses.
func (p *customerAuthProcessor) registrationOrganization(ctx context.Context, requested string) (uuid.UUID, error) {
requested = strings.TrimSpace(requested)
if requested != "" {
id, err := uuid.Parse(requested)
if err != nil {
return uuid.Nil, fmt.Errorf("organization_id must be a valid UUID")
}
exists, err := p.customerAuthRepo.OrganizationExists(ctx, id)
if err != nil {
return uuid.Nil, err
}
if !exists {
return uuid.Nil, fmt.Errorf("organization not found")
}
return id, nil
}
ids, err := p.customerAuthRepo.OrganizationIDs(ctx, 2)
if err != nil {
return uuid.Nil, err
}
switch len(ids) {
case 1:
return ids[0], nil
case 0:
return uuid.Nil, fmt.Errorf("no organization exists to register customers into")
default:
return uuid.Nil, fmt.Errorf("organization_id is required: there is more than one organization")
}
}
@@ -19,6 +19,8 @@ type CustomerAuthRepository interface {
SetCustomerPassword(ctx context.Context, customerID string, passwordHash string) error
// OrganizationExists reports whether an organization with this id exists.
OrganizationExists(ctx context.Context, organizationID uuid.UUID) (bool, error)
// OrganizationIDs returns up to limit organization ids.
OrganizationIDs(ctx context.Context, limit int) ([]uuid.UUID, error)
}
type customerAuthRepository struct {
@@ -90,3 +92,12 @@ func (r *customerAuthRepository) OrganizationExists(ctx context.Context, organiz
}
return count > 0, nil
}
func (r *customerAuthRepository) OrganizationIDs(ctx context.Context, limit int) ([]uuid.UUID, error) {
var ids []uuid.UUID
err := r.db.WithContext(ctx).Table("organizations").Order("created_at").Limit(limit).Pluck("id", &ids).Error
if err != nil {
return nil, fmt.Errorf("failed to list organizations: %w", err)
}
return ids, nil
}
@@ -71,8 +71,10 @@ func (v *CustomerAuthValidatorImpl) ValidateRegisterStartRequest(req *contract.R
}
// Validate organization
if _, err := uuid.Parse(strings.TrimSpace(req.OrganizationID)); err != nil {
return errors.New("organization_id must be a valid UUID"), constants.ValidationErrorCode
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