modules/orgs
Package orgs holds the building blocks for organisations in multi-tenant apps (ADR-0023, ADR-0048): organisation IDs, the membership check every organisation operation starts with, and invitation emails. The generated app owns the flows, tables and SQL in internal/modules/orgs.
Stability: pre-1.0 (ADR-0015).
Constants#
const RoleOwner, …#
const (
RoleOwner = "owner"
RoleAdmin = "admin"
RoleMember = "member"
)The organisation roles every app declares in its org catalog. Apps may add their own. Role names are public API.
Variables#
var ErrOrgNotFound, …#
var (
// ErrOrgNotFound reports an organisation that doesn't exist, is deleted,
// or that the caller isn't a member of. The three look the same, so
// organisation IDs can't be probed.
ErrOrgNotFound = errors.New("orgs: organisation not found")
// ErrNotMember is returned by [Memberships] when the user isn't a
// member of a live organisation. [RequireMember] turns it into
// [ErrOrgNotFound].
ErrNotMember = errors.New("orgs: not a member")
// ErrInvalidID reports a string that isn't an organisation ID.
ErrInvalidID = errors.New("orgs: invalid organisation ID")
)Errors. Check them with errors.Is.
Types#
type Emails#
type Emails interface {
// SendInvitation invites to to join an organisation.
SendInvitation(ctx context.Context, to string, inv Invitation) error
}Emails sends the emails organisations need. Apps use NewMailEmails or their own templates. Implementations should queue rather than deliver inline (jobs.AsyncSender), so requests don't wait for the provider.
func NewMailEmails#
func NewMailEmails(sender mail.Sender, appName string) EmailsNewMailEmails returns plain, readable emails sent through sender, which sets the From address (mail.WithDefaults). appName appears in subjects and bodies.
type ID#
type ID stringID identifies an organisation, such as org_2x7…. It is a distinct type so a user ID can't be passed where an organisation ID is expected.
func NewID#
func NewID() IDNewID returns a random organisation ID with 128 bits of randomness.
func ParseID#
func ParseID(s string) (ID, error)ParseID returns s as an ID, or ErrInvalidID when s isn't shaped like one. It doesn't check that the organisation exists.
func (ID) String#
func (id ID) String() stringString returns the ID as stored.
type Invitation#
type Invitation struct {
// OrgName is the organisation's name and InvitedBy who sent the
// invitation (a name or email address).
OrgName string
InvitedBy string
// Role is the role the invited person gets.
Role string
// URL opens the invitation in the app's frontend; it carries the token.
URL string
// ExpiresIn is how long the invitation stays valid.
ExpiresIn time.Duration
}Invitation is what an invitation email says.
type Member#
type Member struct {
OrgID ID
UserID string
Role string
}Member is a user's membership in an organisation.
func RequireMember#
func RequireMember(ctx context.Context, m Memberships, catalog *auth.Catalog, orgID ID, permission string) (context.Context, Member, error)RequireMember checks that the signed-in user is a member of orgID whose role grants permission, and returns a context whose actor acts in that organisation: OrgID is set and Permissions are the role's permissions in catalog, replacing platform permissions for the operation. Every organisation operation calls it first (ADR-0048).
It returns actor.ErrUnauthenticated without a signed-in user, ErrOrgNotFound when orgID isn't one of the user's live organisations, actor.ErrStepUpRequired when the role grants permission only to sessions verified with a second factor, and actor.ErrForbidden when it doesn't grant it.
The membership is read on every call, so removing a member takes effect on their next request.
type Memberships#
type Memberships interface {
// MemberRole returns userID's role in orgID, or [ErrNotMember] when the
// user isn't a member or the organisation is deleted.
MemberRole(ctx context.Context, orgID ID, userID string) (string, error)
}Memberships reads memberships. The app's orgs repository implements it.