Add atomic owned organization creation
verify / verify (push) Successful in 3m48s

This commit is contained in:
2026-09-04 23:19:05 -04:00
parent b1710e08b8
commit ed0cc8ceff
11 changed files with 586 additions and 18 deletions
+23 -13
View File
@@ -28,6 +28,7 @@ var (
ErrPersonalOrganization = errors.New("organizations: personal organization lifecycle is fixed")
ErrLastOwner = errors.New("organizations: the last active direct owner must be preserved")
ErrOwnerAuthority = errors.New("organizations: a current direct owner must manage owner access")
ErrOwnedCreationUnsupported = errors.New("organizations: atomic owned organization creation is unsupported")
slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,62}$`)
idPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{8,128}$`)
)
@@ -156,22 +157,11 @@ func New(repository Repository, options Options) (*Service, error) {
type CreateOrganization struct {
Slug, Name, OwnerUserID string
Personal bool
RequestID string
}
func (service *Service) CreateOrganization(ctx context.Context, input CreateOrganization) (Organization, error) {
input.Slug = strings.ToLower(strings.TrimSpace(input.Slug))
input.Name = strings.TrimSpace(input.Name)
if !slugPattern.MatchString(input.Slug) || !bounded(input.Name, 128) || !idPattern.MatchString(input.OwnerUserID) {
return Organization{}, errors.New("organizations: invalid organization")
}
id, err := token(service.random, 18)
if err != nil {
return Organization{}, err
}
now := service.now().UTC()
organization := Organization{ID: id, Slug: input.Slug, Name: input.Name, Status: "active", Personal: input.Personal, Revision: 1, CreatedAt: now, UpdatedAt: now}
owner := Membership{OrganizationID: id, UserID: input.OwnerUserID, Status: "active", JoinedAt: now}
audit, err := service.audit(input.OwnerUserID, id, "organization.create", "organization", id, "Organization created")
organization, owner, audit, err := service.prepareOrganization(input)
if err != nil {
return Organization{}, err
}
@@ -181,6 +171,26 @@ func (service *Service) CreateOrganization(ctx context.Context, input CreateOrga
return organization, nil
}
func (service *Service) prepareOrganization(input CreateOrganization) (Organization, Membership, AuditEvent, error) {
input.Slug = strings.ToLower(strings.TrimSpace(input.Slug))
input.Name = strings.TrimSpace(input.Name)
if !slugPattern.MatchString(input.Slug) || !bounded(input.Name, 128) || !idPattern.MatchString(input.OwnerUserID) || !boundedOptional(input.RequestID, 128) {
return Organization{}, Membership{}, AuditEvent{}, errors.New("organizations: invalid organization")
}
id, err := token(service.random, 18)
if err != nil {
return Organization{}, Membership{}, AuditEvent{}, err
}
now := service.now().UTC()
organization := Organization{ID: id, Slug: input.Slug, Name: input.Name, Status: "active", Personal: input.Personal, Revision: 1, CreatedAt: now, UpdatedAt: now}
owner := Membership{OrganizationID: id, UserID: input.OwnerUserID, Status: "active", JoinedAt: now}
audit, err := service.auditWithRequest(input.OwnerUserID, id, "organization.create", "organization", id, input.RequestID, "Organization created")
if err != nil {
return Organization{}, Membership{}, AuditEvent{}, err
}
return organization, owner, audit, nil
}
func (service *Service) CreatePersonalOrganization(ctx context.Context, userID, displayName string) (Organization, error) {
value := make([]byte, 6)
if _, err := io.ReadFull(service.random, value); err != nil {