apistockdocs
v0.4 GitHub apistock.dev
Technical/Module packages

modules/orgs

import "apistock.dev/modules/orgs"Source on GitHub

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, …#

go
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, …#

go
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#

go
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#

go
func NewMailEmails(sender mail.Sender, appName string) Emails

NewMailEmails returns plain, readable emails sent through sender, which sets the From address (mail.WithDefaults). appName appears in subjects and bodies.

type ID#

go
type ID string

ID 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#

go
func NewID() ID

NewID returns a random organisation ID with 128 bits of randomness.

func ParseID#

go
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#

go
func (id ID) String() string

String returns the ID as stored.

type Invitation#

go
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#

go
type Member struct {
	OrgID  ID
	UserID string
	Role   string
}

Member is a user's membership in an organisation.

func RequireMember#

go
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#

go
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.

v0.4
esc
↑↓ move↵ openesc close