// SPDX-License-Identifier: MPL-2.0 package organizations import ( "context" "errors" "gamertan.com/web/access" ) // OwnedOrganization is one atomic creation command. Implementations must commit // the organization, membership, direct owner binding, and both audits together. type OwnedOrganization struct { Organization Organization Membership Membership OwnerBinding access.Binding OrganizationAudit AuditEvent AccessAudit access.AuditEvent } // OwnedOrganizationRepository extends Repository without changing the legacy // membership-only CreateOrganization contract. There is no non-atomic fallback. type OwnedOrganizationRepository interface { CreateOwnedOrganization(context.Context, OwnedOrganization) error } var ErrOwnedManagementUnsupported = errors.New("organizations: atomic owner-managed updates are unsupported") // OwnerManagedRepository rechecks the configured direct owner in the same // transaction as profile and membership writes. These explicit operations do // not change legacy methods used by applications with delegated administrators. // Implementations must also preserve optimistic state, last-owner protection, // and audit atomicity. There is no preflight-only fallback. type OwnerManagedRepository interface { UpdateOwnedOrganization(context.Context, Organization, int64, string, AuditEvent) error ChangeOwnedMembershipStatus(context.Context, MembershipStatusChange, string, AuditEvent) error RemoveOwnedMembershipIfCurrent(context.Context, MembershipRemoval, string, AuditEvent) error } // UpdateOwnedOrganization changes a non-personal organization's name and slug. // OwnerRole comes from trusted service configuration, not submitted form data. func (service *Service) UpdateOwnedOrganization(ctx context.Context, input UpdateOrganization) (Organization, error) { return service.updateOrganization(ctx, input, true) } // ChangeOwnedMembershipStatus requires a current owner even when the target // member is not an owner. It preserves the last active owner. func (service *Service) ChangeOwnedMembershipStatus(ctx context.Context, input MembershipStatusChange) error { return service.changeMembershipStatus(ctx, input, true) } // RemoveOwnedMembershipIfCurrent removes only the displayed membership state, // with current owner authority checked in the write transaction. func (service *Service) RemoveOwnedMembershipIfCurrent(ctx context.Context, input MembershipRemoval) error { return service.removeMembershipIfCurrent(ctx, input, true) } // CreateOwnedOrganization grants the configured OwnerRole to the initial owner // inside the creation transaction. Applications authorize creation and choose // OwnerRole when constructing the service, never from a submitted role name. // The role must already be seeded in the repository. func (service *Service) CreateOwnedOrganization(ctx context.Context, input CreateOrganization) (Organization, error) { if service.ownerRole == "" { return Organization{}, errors.New("organizations: owned creation requires a configured owner role") } repository, ok := service.repository.(OwnedOrganizationRepository) if !ok { return Organization{}, ErrOwnedCreationUnsupported } organization, membership, audit, err := service.prepareOrganization(input) if err != nil { return Organization{}, err } bindingID, err := token(service.random, 18) if err != nil { return Organization{}, err } accessAuditID, err := token(service.random, 18) if err != nil { return Organization{}, err } binding := access.Binding{ ID: bindingID, SubjectKind: access.User, SubjectID: input.OwnerUserID, Role: service.ownerRole, Scope: access.Scope{OrganizationID: organization.ID}, GrantedBy: input.OwnerUserID, GrantedAt: organization.CreatedAt, } accessAudit := access.AuditEvent{ ID: accessAuditID, OrganizationID: organization.ID, ActorUserID: input.OwnerUserID, Action: "access.binding.grant", ResourceType: "binding", ResourceID: bindingID, RequestID: input.RequestID, Summary: "Initial organization owner granted", CreatedAt: organization.CreatedAt, } if err = repository.CreateOwnedOrganization(ctx, OwnedOrganization{ Organization: organization, Membership: membership, OwnerBinding: binding, OrganizationAudit: audit, AccessAudit: accessAudit, }); err != nil { return Organization{}, err } return organization, nil }