Documentation
¶
Overview ¶
Package organization adds workspaces and membership to the auth core.
Unlike the phone and username plugins this one owns real tables, because a workspace is not a way of naming a user — it is a separate entity with its own lifecycle. It is also the plugin that demonstrates the join a plugin makes back INTO the core: a member list is meaningless without the email address that lives on auth_user.
Why the invite lookup is the important code here ¶
The published account-takeover advisories in comparable auth libraries are overwhelmingly invitation bugs: an invitation addressed to an email is matched against an identity the claimant never proved they own. The fix is one predicate, and it is applied in PendingInvitesQuery below.
Example (RelationAuthorizer) ¶
Example_relationAuthorizer plugs a Zanzibar-style relationship check into the organization plugin: alice may delete members of acme because a tuple says so, whatever her role.
package main
import (
"context"
"fmt"
"github.com/ubgo/auth/plugin/access"
"github.com/ubgo/auth/plugin/organization"
)
// tupleStore is a stand-in for a relationship store such as OpenFGA or
// SpiceDB: a set of (user, relation, object) tuples. With OpenFGA, Check
// becomes the client's Check call, and its error is returned so a store
// that is down refuses.
type tupleStore map[[3]string]bool
func (s tupleStore) Check(_ context.Context, user, relation, object string) (bool, error) {
return s[[3]string{user, relation, object}], nil
}
// relationAuthorizer maps each permission to a relation on the organization
// object: "can_<action>_<resource>" on "organization:<id>". Every action of
// every resource asked for must hold; Or asks for one resource.
type relationAuthorizer struct{ store tupleStore }
// Authorize asks the store one question per action; an error from the
// store is returned, which refuses.
//
//ds:def id=relationauthorizer-authorize-2qt24ghh owner=@khanakia stability=stable
func (a relationAuthorizer) Authorize(ctx context.Context, req organization.AccessRequest) (bool, error) {
if len(req.Request) == 0 {
return false, nil
}
anyRes := false
for res, want := range req.Request {
ok := len(want.Actions) > 0
for _, act := range want.Actions {
has, err := a.store.Check(ctx, "user:"+req.UserID, "can_"+act+"_"+res, "organization:"+req.OrgID)
if err != nil {
return false, err
}
if want.Connector == access.Or && has {
ok = true
break
}
ok = ok && has
}
if !ok && req.Connector != access.Or {
return false, nil
}
anyRes = anyRes || ok
}
return anyRes, nil
}
// Example_relationAuthorizer plugs a Zanzibar-style relationship check into
// the organization plugin: alice may delete members of acme because a tuple
// says so, whatever her role.
func main() {
authz := relationAuthorizer{store: tupleStore{
{"user:alice", "can_delete_member", "organization:acme"}: true,
}}
_ = organization.New(organization.WithAuthorizer(authz))
ask := func(user, org string) bool {
ok, err := authz.Authorize(context.Background(), organization.AccessRequest{OrgID: org, UserID: user,
Request: access.Request{organization.ResourceMember: access.All(organization.ActionDelete)}})
return ok && err == nil
}
fmt.Println(ask("alice", "acme"), ask("alice", "globex"), ask("bob", "acme"))
}
Output: true false false
Index ¶
- Constants
- Variables
- func DefaultRoles() map[Role]access.Role
- func EmailDomain(addr string) string
- func IsDefaultPublicDomain(domain string) bool
- func MembersPageQuery(a *auth.Auth, orgID string, dst *Member, limit, offset int) (sqlb.Query, error)
- func MembersQuery(a *auth.Auth, orgID string, dst *Member) (sqlb.Query, error)
- func MembershipQuery(a *auth.Auth, orgID, userID string, dst *Role) (sqlb.Query, error)
- func NormalizeDomain(raw string) (string, error)
- func PendingInvitesQuery(a *auth.Auth, userID string, now time.Time, dst *Invite) (sqlb.Query, error)
- func UserBindings(dst *UserMembership) []sqlb.Binding
- func UserJoin(orgID string) sqlb.Join
- type AcceptRequest
- type AccessRequest
- type ActiveResult
- type AddDomainRequest
- type AddMemberRequest
- type AllOrgsRequest
- type AllOrgsResult
- type Authorizer
- type AuthorizerFunc
- type CreateRequest
- type CreateRoleRequest
- type CreateTeamRequest
- type DNSVerifier
- type Direction
- type DomainChallenge
- type DomainRequest
- type DomainSuggestion
- type DomainVerifier
- type DomainVerifierFunc
- type DomainsConfig
- type DomainsRequest
- type DomainsResult
- type DynamicRolesConfig
- type EnrollmentMode
- type Field
- type FieldsConfig
- type FilterOperator
- type FullOrg
- type GetRequest
- type Hooks
- type Invitation
- type InvitationData
- type InvitationsResult
- type Invite
- type InviteMail
- type InviteRequest
- type InviteStatus
- type InvitesResult
- type ListResult
- type Member
- type MemberChange
- type MemberField
- type MemberRequest
- type MemberSort
- type MemberView
- type MembersResult
- type MembershipRequest
- type MembershipRequestsResult
- type Option
- func CreateGate(fn func(ctx context.Context, u auth.User) (bool, error)) Option
- func CreatorRole(r Role) Option
- func Domains(cfg DomainsConfig) Option
- func DynamicRoles(cfg DynamicRolesConfig) Option
- func InvitationLimit(n int) Option
- func InviteBy(kinds ...auth.IdentifierKindName) Option
- func InviteLimit(limit int, window time.Duration) Option
- func InviteLink(fn func() string) Option
- func InviteMessage(fn func(m InviteMail) (subject, text string)) Option
- func MaxMembers(n int) Option
- func MaxOwned(n int) Option
- func RefuseRepeatInvites() Option
- func RejectExistingMembers() Option
- func ReplacePendingInvites() Option
- func Teams(cfg TeamsConfig) Option
- func WithAccessControl(ac *access.AccessControl) Option
- func WithAuthorizer(a Authorizer) Option
- func WithFields(cfg FieldsConfig) Option
- func WithHooks(h Hooks) Option
- func WithHooksFrom(build func(a *auth.Auth) Hooks) Option
- func WithInviteTTL(d time.Duration) Option
- func WithRoles(roles map[Role]access.Role) Option
- func WithoutInvitations() Option
- type Org
- type OrgDomain
- type OrgRequest
- type OrgRole
- type OrgSummary
- type OrgUpdate
- type OrgWithRole
- type PermissionDoc
- type PermissionRequest
- type PermissionResult
- type Plugin
- func (p *Plugin) APIKeyPermission(ctx context.Context, orgID, userID, action string) (member, allowed bool, err error)
- func (p *Plugin) Accept(ctx context.Context, inviteID string) (Org, error)
- func (p *Plugin) AcceptSuggestion(ctx context.Context, domainID string) (MembershipRequest, error)
- func (p *Plugin) Active(ctx context.Context) (ActiveResult, error)
- func (p *Plugin) ActiveOrgID(ctx context.Context) (id string, ok bool, err error)
- func (p *Plugin) AddDomain(ctx context.Context, in AddDomainRequest) (OrgDomain, error)
- func (p *Plugin) AddMember(ctx context.Context, orgID, userID string, role Role) error
- func (p *Plugin) AddMemberWith(ctx context.Context, in AddMemberRequest) error
- func (p *Plugin) AddTeamMember(ctx context.Context, in TeamMemberRequest) error
- func (p *Plugin) ApproveMembershipRequest(ctx context.Context, requestID string) error
- func (p *Plugin) Can(ctx context.Context, orgID, userID string, req access.Request, ...) (bool, error)
- func (p *Plugin) Cancel(ctx context.Context, inviteID string) error
- func (p *Plugin) CancelFor(ctx context.Context, inviteID string, by auth.User) error
- func (p *Plugin) CheckSlug(ctx context.Context, slug string) (SlugResult, error)
- func (p *Plugin) Create(ctx context.Context, in CreateRequest) (Org, error)
- func (p *Plugin) CreateFor(ctx context.Context, userID string, in CreateRequest) (o Org, err error)
- func (p *Plugin) CreateRole(ctx context.Context, in CreateRoleRequest) (OrgRole, error)
- func (p *Plugin) CreateTeam(ctx context.Context, in CreateTeamRequest) (Team, error)
- func (p *Plugin) DataHooks() auth.DataHooks
- func (p *Plugin) DecodeMessageData(purpose auth.Purpose, raw json.RawMessage) (any, error)
- func (p *Plugin) Delete(ctx context.Context, orgID string) error
- func (p *Plugin) DeleteFor(ctx context.Context, orgID string) error
- func (p *Plugin) DeleteRole(ctx context.Context, in RoleRequest) error
- func (p *Plugin) DomainSuggestions(ctx context.Context) ([]DomainSuggestion, error)
- func (p *Plugin) Get(ctx context.Context, in GetRequest) (Org, error)
- func (p *Plugin) GetFor(ctx context.Context, orgID string) (Org, error)
- func (p *Plugin) GetFull(ctx context.Context, in GetRequest) (FullOrg, error)
- func (p *Plugin) GetRole(ctx context.Context, in RoleRequest) (OrgRole, error)
- func (p *Plugin) HasPermission(ctx context.Context, in PermissionRequest) (bool, error)
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) Invitation(ctx context.Context, inviteID string) (Invite, error)
- func (p *Plugin) InvitationsFor(ctx context.Context, orgID string) ([]Invitation, error)
- func (p *Plugin) Invite(ctx context.Context, in InviteRequest) error
- func (p *Plugin) Invites(ctx context.Context) ([]Invite, error)
- func (p *Plugin) List(ctx context.Context) ([]OrgWithRole, error)
- func (p *Plugin) ListAll(ctx context.Context, in AllOrgsRequest) (AllOrgsResult, error)
- func (p *Plugin) ListDomains(ctx context.Context, orgID string) ([]OrgDomain, error)
- func (p *Plugin) ListInvitations(ctx context.Context, orgID string) ([]Invitation, error)
- func (p *Plugin) ListMembers(ctx context.Context, in OrgRequest) (MembersResult, error)
- func (p *Plugin) ListMembershipRequests(ctx context.Context, orgID string) ([]MembershipRequest, error)
- func (p *Plugin) ListRoles(ctx context.Context, orgID string) ([]OrgRole, error)
- func (p *Plugin) ListTeamMembers(ctx context.Context, teamID string) ([]TeamMember, error)
- func (p *Plugin) ListTeams(ctx context.Context, orgID string) ([]Team, error)
- func (p *Plugin) ListUserTeams(ctx context.Context) ([]Team, error)
- func (p *Plugin) MarkDomainVerified(ctx context.Context, domainID string) (OrgDomain, error)
- func (p *Plugin) Members(ctx context.Context, orgID string) ([]MemberView, error)
- func (p *Plugin) MembersFor(ctx context.Context, in OrgRequest) (MembersResult, error)
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) PermissionsOf(ctx context.Context, orgID string, role Role) (access.Permissions, error)
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) Reject(ctx context.Context, inviteID string) error
- func (p *Plugin) RejectMembershipRequest(ctx context.Context, requestID string) error
- func (p *Plugin) RemoveDomain(ctx context.Context, domainID string) error
- func (p *Plugin) RemoveMember(ctx context.Context, in MemberRequest) error
- func (p *Plugin) RemoveMemberFor(ctx context.Context, orgID, userID string) error
- func (p *Plugin) RemoveTeam(ctx context.Context, teamID string) error
- func (p *Plugin) RemoveTeamMember(ctx context.Context, in TeamMemberRequest) error
- func (p *Plugin) RequirePermission(orgID func(r *http.Request) string, req access.Request) func(http.Handler) http.Handler
- func (p *Plugin) RequireRole(orgID func(r *http.Request) string, role Role) func(http.Handler) http.Handler
- func (p *Plugin) Requires() []string
- func (p *Plugin) Role(ctx context.Context, orgID string) (Role, error)
- func (p *Plugin) RoleAuthorizer() Authorizer
- func (p *Plugin) RoleOf(ctx context.Context, orgID, userID string) (Role, error)
- func (p *Plugin) SessionEnded(ctx context.Context, userID, id string) error
- func (p *Plugin) SessionReplaced(ctx context.Context, old, new auth.Session) error
- func (p *Plugin) SetActive(ctx context.Context, in SetActiveRequest) (ActiveResult, error)
- func (p *Plugin) SetActiveTeam(ctx context.Context, teamID string) (ActiveResult, error)
- func (p *Plugin) SetRole(ctx context.Context, in SetRoleRequest) error
- func (p *Plugin) SetRoleFor(ctx context.Context, orgID, userID string, role Role) error
- func (p *Plugin) Sweep(ctx context.Context, cutoff time.Time) (int64, error)
- func (p *Plugin) Tables() []sqlb.Table
- func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)
- func (p *Plugin) Update(ctx context.Context, in UpdateRequest) (Org, error)
- func (p *Plugin) UpdateDomain(ctx context.Context, in UpdateDomainRequest) (OrgDomain, error)
- func (p *Plugin) UpdateFor(ctx context.Context, orgID string, data OrgUpdate) (o Org, err error)
- func (p *Plugin) UpdateRole(ctx context.Context, in UpdateRoleRequest) (OrgRole, error)
- func (p *Plugin) UpdateTeam(ctx context.Context, in UpdateTeamRequest) (Team, error)
- func (p *Plugin) UserSessionsEnded(ctx context.Context, userID, exceptID string) error
- func (p *Plugin) UserTables() []auth.UserTable
- func (p *Plugin) Validate(c *auth.Config) error
- func (p *Plugin) VerifyDomain(ctx context.Context, domainID string) (OrgDomain, error)
- type RequestIDRequest
- type RequestStatus
- type Role
- type RoleRequest
- type RoleResult
- type RoleUpdate
- type RolesResult
- type SetActiveRequest
- type SetRoleRequest
- type SlugRequest
- type SlugResult
- type SuggestionsResult
- type TXTResolver
- type Team
- type TeamMember
- type TeamMemberChange
- type TeamMemberRequest
- type TeamMembersResult
- type TeamRequest
- type TeamUpdate
- type TeamsConfig
- type TeamsResult
- type UpdateDomainRequest
- type UpdateRequest
- type UpdateRoleRequest
- type UpdateTeamRequest
- type UserMembership
Examples ¶
Constants ¶
const ( TableNameDomain = "auth_org_domain" TableNameDomainRequest = "auth_org_domain_request" TableAliasDomain = "odm" TableAliasDomainRequest = "odr" )
Tables of verified domains, declared only when Domains is on.
const ( ColDomainID = "id" ColDomainOrgID = "org_id" ColDomainName = "domain" ColDomainVerified = "verified_domain" ColDomainEnrollment = "enrollment" ColDomainRole = "role" ColDomainToken = "token" ColDomainCreatedBy = "created_by" ColDomainVerifiedAt = "verified_at" ColDomainCreatedAt = "created_at" )
Columns of auth_org_domain.
const ( ColRequestID = "id" ColRequestOrgID = "org_id" ColRequestUserID = "user_id" ColRequestDomainID = "domain_id" ColRequestStatus = "status" ColRequestCreatedAt = "created_at" ColRequestDecidedAt = "decided_at" ColRequestDecidedBy = "decided_by" )
Columns of auth_org_domain_request.
const ( EndpointAddDomain auth.EndpointID = "organization.add-domain" EndpointListDomains auth.EndpointID = "organization.list-domains" EndpointVerifyDomain auth.EndpointID = "organization.verify-domain" EndpointUpdateDomain auth.EndpointID = "organization.update-domain" EndpointRemoveDomain auth.EndpointID = "organization.remove-domain" EndpointSuggestions auth.EndpointID = "organization.domain-suggestions" EndpointAcceptSuggest auth.EndpointID = "organization.accept-suggestion" EndpointListRequests auth.EndpointID = "organization.list-membership-requests" EndpointApproveRequest auth.EndpointID = "organization.approve-membership-request" EndpointRejectRequest auth.EndpointID = "organization.reject-membership-request" )
Operation ids of verified domains, permanent like the others.
const ( // DefaultDomainRecordPrefix is the label the TXT record lives under: // _auth-verification.acme.com. An underscore label is never a host name, // so the record cannot collide with a real one. DefaultDomainRecordPrefix = "_auth-verification" // DomainRecordValuePrefix starts the TXT record's value, so a person // reading the zone knows what the record is for. DomainRecordValuePrefix = "auth-domain-verification" // DefaultMaxDomains caps the domains of one organization (Clerk's limit): // enough for a company with a few brands, and a cap so one admin cannot // fill the table. DomainsConfig.MaxDomains changes it. DefaultMaxDomains = 10 // DefaultVerifyLimit and DefaultVerifyWindow bound verify-domain per // client address: each call is a DNS lookup, and 10 a minute is plenty for // someone waiting on propagation. DefaultVerifyLimit = 10 DefaultVerifyWindow = time.Minute )
const ( TableNameOrg = "auth_org" TableNameMember = "auth_org_member" TableNameInvite = "auth_org_invite" // TableNameTeam and TableNameTeamMember hold teams, created only with // the Teams option. TableNameTeam = "auth_org_team" TableNameTeamMember = "auth_org_team_member" // TableNameRole holds roles created at run time per organization, // created only with the DynamicRoles option. TableNameRole = "auth_org_role" // TableNameActive holds each session's active organization and team. // Keyed by session id rather than a column on auth_session, so it works // with every session store, as plugin/sessiondata does. TableNameActive = "auth_org_active" )
Table names owned by this plugin.
const ( TableAliasOrg = "og" TableAliasMember = "om" TableAliasInvite = "oi" TableAliasIdentity = "oid" TableAliasTeam = "ot" TableAliasTeamMember = "otm" TableAliasRole = "orl" TableAliasActive = "oa" )
Table aliases owned by this plugin. None may collide with the core's reserved aliases or another plugin's; auth.New rejects a collision at wire time rather than letting it surface as an ambiguous column at query time.
const ( ColOrgID = "id" ColOrgSlug = "slug" ColOrgName = "name" ColOrgCreatedAt = "created_at" ColOrgLogo = "logo" ColOrgMetadata = "metadata" )
Column names on auth_org.
const ( ColMemberOrgID = "org_id" ColMemberUserID = "user_id" ColMemberRole = "role" ColMemberJoinedAt = "joined_at" )
Column names on auth_org_member.
const ( ColInviteID = "id" ColInviteOrgID = "org_id" // ColInviteEmail holds the invited address when the invitation is // addressed by email, and "" for any other kind: better-auth's email // field, kept so a reader of email invitations needs no change. ColInviteEmail = "email" // ColInviteKind and ColInviteIdentifier address the invitation for every // kind (InviteBy): the identifier kind and its normalized value. They are // the pair PendingInvitesQuery matches against a verified identity; for // an email invitation ColInviteIdentifier equals ColInviteEmail. ColInviteKind = "identifier_kind" ColInviteIdentifier = "identifier" ColInviteRole = "role" ColInviteInvitedBy = "invited_by" ColInviteExpiresAt = "expires_at" ColInviteAccepted = "accepted_at" ColInviteCreatedAt = "created_at" ColInviteStatus = "status" ColInviteTeamID = "team_id" )
Column names on auth_org_invite.
const ( ColTeamID = "id" ColTeamOrgID = "org_id" ColTeamName = "name" ColTeamCreatedAt = "created_at" ColTeamUpdatedAt = "updated_at" )
Column names on auth_org_team.
const ( ColTeamMemberTeamID = "team_id" ColTeamMemberUserID = "user_id" ColTeamMemberCreatedAt = "created_at" )
Column names on auth_org_team_member.
const ( ColRoleID = "id" ColRoleOrgID = "org_id" ColRoleName = "role" ColRolePermission = "permission" ColRoleCreatedAt = "created_at" ColRoleUpdatedAt = "updated_at" )
Column names on auth_org_role.
const ( ColActiveSessionID = "session_id" ColActiveUserID = "user_id" ColActiveOrgID = "org_id" ColActiveTeamID = "team_id" ColActiveExpiresAt = "expires_at" )
Column names on auth_org_active.
const ( EndpointCreate auth.EndpointID = "organization.create" EndpointList auth.EndpointID = "organization.list" EndpointMembers auth.EndpointID = "organization.members" EndpointInvite auth.EndpointID = "organization.invite" EndpointInvites auth.EndpointID = "organization.invites" EndpointAccept auth.EndpointID = "organization.accept" EndpointRemoveMember auth.EndpointID = "organization.remove-member" EndpointSetRole auth.EndpointID = "organization.set-role" EndpointCheckSlug auth.EndpointID = "organization.check-slug" EndpointInvitation auth.EndpointID = "organization.invitation" EndpointRole auth.EndpointID = "organization.role" EndpointGet auth.EndpointID = "organization.get" EndpointGetFull auth.EndpointID = "organization.get-full" EndpointUpdate auth.EndpointID = "organization.update" EndpointDelete auth.EndpointID = "organization.delete" EndpointSetActive auth.EndpointID = "organization.set-active" EndpointActive auth.EndpointID = "organization.active" EndpointReject auth.EndpointID = "organization.reject" EndpointCancel auth.EndpointID = "organization.cancel" EndpointListInvitations auth.EndpointID = "organization.list-invitations" EndpointHasPermission auth.EndpointID = "organization.has-permission" EndpointCreateTeam auth.EndpointID = "organization.create-team" EndpointListTeams auth.EndpointID = "organization.list-teams" EndpointUpdateTeam auth.EndpointID = "organization.update-team" EndpointRemoveTeam auth.EndpointID = "organization.remove-team" EndpointSetActiveTeam auth.EndpointID = "organization.set-active-team" EndpointListUserTeams auth.EndpointID = "organization.list-user-teams" EndpointListTeamMembers auth.EndpointID = "organization.list-team-members" EndpointAddTeamMember auth.EndpointID = "organization.add-team-member" EndpointRemoveTeamMember auth.EndpointID = "organization.remove-team-member" EndpointCreateRole auth.EndpointID = "organization.create-role" EndpointUpdateRole auth.EndpointID = "organization.update-role" EndpointDeleteRole auth.EndpointID = "organization.delete-role" EndpointGetRole auth.EndpointID = "organization.get-role" EndpointListRoles auth.EndpointID = "organization.list-roles" )
Operation ids. They are permanent: rate rules, hooks and every transport route address an operation by id, so renaming one breaks deployed clients.
const ( // DefaultInviteLimit and DefaultInviteWindow bound invitation mail per // client address: 50 an hour lets an admin onboard a team in one sitting // and keeps a stolen session from turning the app's mail domain into a // spam relay. InviteLimit changes both. //ds:def id=invitelimit-6wd9gmj5 owner=@khanakia stability=stable DefaultInviteLimit = 50 //ds:def id=invitewindow-fhrf25ap owner=@khanakia stability=stable DefaultInviteWindow = time.Hour // DefaultMembersPage and MaxMembersPage bound the members list: a page // an org settings screen shows, and a cap so one request cannot read a // huge organization at once. A larger limit is clamped, not refused. DefaultMembersPage = 50 MaxMembersPage = 200 // DefaultInvitationLimit is better-auth's invitationLimit: at most this // many pending invitations per organization, so a stolen admin session // cannot fill the table. InvitationLimit changes it; zero is no cap. DefaultInvitationLimit = 100 )
const ( ResourceOrganization = "organization" ResourceMember = "member" ResourceInvitation = "invitation" ResourceTeam = "team" // ResourceAC is access control itself: the dynamic roles. ResourceAC = "ac" )
Resources of DefaultStatements, the ones the plugin's own operations check.
const ( ActionCreate = "create" ActionRead = "read" ActionUpdate = "update" ActionDelete = "delete" ActionCancel = "cancel" )
Actions of DefaultStatements.
const DefaultInvitePage = "/invitations"
DefaultInvitePage is the app page an invitation mail links to; it lists the signed-in user's invitations through organization.invites.
const DefaultInviteTTL = 7 * 24 * time.Hour
DefaultInviteTTL is how long an invitation stays acceptable: a week covers an invitee who is away for a few days, and anything longer leaves a forgotten invitation usable by whoever controls that inbox months later.
const MaxMetadataSize = 16 << 10
MaxMetadataSize bounds an organization's metadata, in bytes: room for settings and identifiers, not for documents, which belong in the app's own tables keyed by the organization id.
const PluginName = "organization"
PluginName is the stable identity of this plugin.
const PurposeInvite auth.Purpose = "organization.invite"
PurposeInvite is the message sent to an invited address. A mailer switches on it to pick the invitation template.
Variables ¶
var ( TableDomain = sqlb.Table{Name: TableNameDomain, Alias: TableAliasDomain} TableDomainRequest = sqlb.Table{Name: TableNameDomainRequest, Alias: TableAliasDomainRequest} DomainID = sqlb.Col[string](TableDomain, ColDomainID) DomainOrgID = sqlb.Col[string](TableDomain, ColDomainOrgID) DomainName = sqlb.Col[string](TableDomain, ColDomainName) DomainVerified = sqlb.Col[*string](TableDomain, ColDomainVerified) DomainEnrollment = sqlb.Col[EnrollmentMode](TableDomain, ColDomainEnrollment) DomainRole = sqlb.Col[Role](TableDomain, ColDomainRole) DomainToken = sqlb.Col[string](TableDomain, ColDomainToken) DomainCreatedBy = sqlb.Col[string](TableDomain, ColDomainCreatedBy) DomainVerifiedAt = sqlb.Col[*time.Time](TableDomain, ColDomainVerifiedAt) DomainCreatedAt = sqlb.Col[time.Time](TableDomain, ColDomainCreatedAt) RequestID = sqlb.Col[string](TableDomainRequest, ColRequestID) RequestOrgID = sqlb.Col[string](TableDomainRequest, ColRequestOrgID) RequestUserID = sqlb.Col[string](TableDomainRequest, ColRequestUserID) RequestDomainID = sqlb.Col[string](TableDomainRequest, ColRequestDomainID) RequestStatusCol = sqlb.Col[RequestStatus](TableDomainRequest, ColRequestStatus) RequestCreatedAt = sqlb.Col[time.Time](TableDomainRequest, ColRequestCreatedAt) RequestDecidedAt = sqlb.Col[*time.Time](TableDomainRequest, ColRequestDecidedAt) RequestDecidedBy = sqlb.Col[*string](TableDomainRequest, ColRequestDecidedBy) )
Domain tables and typed columns, for an app that joins them; reads are open, writes go through the operations.
var ( // ErrDomainNotFound is an unknown domain id, or one of an organization // the caller is not in; one error for both, so a guessed id says nothing. ErrDomainNotFound = auth.NewError("organization.domain_not_found", http.StatusNotFound, "Not found.") // ErrBadDomain is a value that is not a domain name. ErrBadDomain = auth.NewError("organization.bad_domain", http.StatusBadRequest, "Enter a domain such as example.com.") // ErrPublicDomain is a domain anyone can get an address at (gmail.com), // which would let every stranger with such an address enroll. ErrPublicDomain = auth.NewError("organization.public_domain", http.StatusBadRequest, "This is a public email domain and cannot belong to an organization.") // ErrDomainTaken is a domain another organization has verified, or one // this organization has already added. ErrDomainTaken = auth.NewError("organization.domain_taken", http.StatusConflict, "This domain is already in use.") // ErrDomainsLimit is an organization with MaxDomains domains. ErrDomainsLimit = auth.NewError("organization.domains_limit", http.StatusConflict, "This organization has reached the number of domains it can add.") // ErrDomainNotVerified answers verify-domain when the record is not // found yet, and an enrollment mode set on a domain not verified. ErrDomainNotVerified = auth.NewError("organization.domain_not_verified", http.StatusConflict, "The domain is not verified yet. Publish the TXT record and try again.") // ErrDomainCheckFailed is a verifier that failed (a DNS outage), as // distinct from one that looked and found no record. ErrDomainCheckFailed = auth.NewError("organization.domain_check_failed", http.StatusServiceUnavailable, "The domain could not be checked. Try again later.") // ErrBadEnrollment is an EnrollmentMode outside EnrollmentModeValues. ErrBadEnrollment = auth.NewError("organization.bad_enrollment", http.StatusBadRequest, "This enrollment mode does not exist.") // ErrDomainRole is the creator role given to a domain: nobody becomes an // owner because of the address they sign up with. ErrDomainRole = auth.NewError("organization.domain_role", http.StatusBadRequest, "A domain cannot grant this role.") // ErrRequestInvalid is a membership request or suggestion that is // unknown, not the caller's to act on, decided already, or whose domain // or address is no longer verified. ErrRequestInvalid = auth.NewError("organization.request_invalid", http.StatusNotFound, "This request is not valid any more.") )
Errors of verified domains.
var ( TableOrg = sqlb.Table{Name: TableNameOrg, Alias: TableAliasOrg} TableMember = sqlb.Table{Name: TableNameMember, Alias: TableAliasMember} TableInvite = sqlb.Table{Name: TableNameInvite, Alias: TableAliasInvite} TableTeam = sqlb.Table{Name: TableNameTeam, Alias: TableAliasTeam} TableTeamMember = sqlb.Table{Name: TableNameTeamMember, Alias: TableAliasTeamMember} TableRole = sqlb.Table{Name: TableNameRole, Alias: TableAliasRole} TableActive = sqlb.Table{Name: TableNameActive, Alias: TableAliasActive} // TableIdentity is the core identity table under this plugin's alias, used // to match an invitation against a verified address. TableIdentity = sqlb.Table{Name: auth.TableNameIdentity, Alias: TableAliasIdentity} )
Tables this plugin declares.
var ( OrgID = sqlb.Col[string](TableOrg, ColOrgID) OrgSlug = sqlb.Col[string](TableOrg, ColOrgSlug) OrgName = sqlb.Col[string](TableOrg, ColOrgName) OrgCreatedAt = sqlb.Col[time.Time](TableOrg, ColOrgCreatedAt) OrgLogo = sqlb.Col[*string](TableOrg, ColOrgLogo) OrgMetadata = sqlb.Col[auth.AppData](TableOrg, ColOrgMetadata) MemberOrgID = sqlb.Col[string](TableMember, ColMemberOrgID) MemberUserID = sqlb.Col[string](TableMember, ColMemberUserID) MemberRole = sqlb.Col[Role](TableMember, ColMemberRole) MemberJoinedAt = sqlb.Col[time.Time](TableMember, ColMemberJoinedAt) InviteID = sqlb.Col[string](TableInvite, ColInviteID) InviteOrgID = sqlb.Col[string](TableInvite, ColInviteOrgID) InviteEmail = sqlb.Col[string](TableInvite, ColInviteEmail) InviteKind = sqlb.Col[auth.IdentifierKindName](TableInvite, ColInviteKind) InviteIdent = sqlb.Col[string](TableInvite, ColInviteIdentifier) InviteRole = sqlb.Col[Role](TableInvite, ColInviteRole) InviteInvitedBy = sqlb.Col[string](TableInvite, ColInviteInvitedBy) InviteExpiresAt = sqlb.Col[time.Time](TableInvite, ColInviteExpiresAt) InviteAccepted = sqlb.Col[*time.Time](TableInvite, ColInviteAccepted) InviteCreatedAt = sqlb.Col[time.Time](TableInvite, ColInviteCreatedAt) InviteStatusCol = sqlb.Col[InviteStatus](TableInvite, ColInviteStatus) InviteTeamID = sqlb.Col[*string](TableInvite, ColInviteTeamID) TeamID = sqlb.Col[string](TableTeam, ColTeamID) TeamOrgID = sqlb.Col[string](TableTeam, ColTeamOrgID) TeamName = sqlb.Col[string](TableTeam, ColTeamName) TeamCreatedAt = sqlb.Col[time.Time](TableTeam, ColTeamCreatedAt) TeamUpdatedAt = sqlb.Col[time.Time](TableTeam, ColTeamUpdatedAt) TeamMemberTeamID = sqlb.Col[string](TableTeamMember, ColTeamMemberTeamID) TeamMemberUserID = sqlb.Col[string](TableTeamMember, ColTeamMemberUserID) TeamMemberCreatedAt = sqlb.Col[time.Time](TableTeamMember, ColTeamMemberCreatedAt) RoleID = sqlb.Col[string](TableRole, ColRoleID) RoleOrgID = sqlb.Col[string](TableRole, ColRoleOrgID) RoleName = sqlb.Col[Role](TableRole, ColRoleName) RolePermission = sqlb.Col[PermissionDoc](TableRole, ColRolePermission) RoleCreatedAt = sqlb.Col[time.Time](TableRole, ColRoleCreatedAt) RoleUpdatedAt = sqlb.Col[time.Time](TableRole, ColRoleUpdatedAt) ActiveSessionID = sqlb.Col[string](TableActive, ColActiveSessionID) ActiveUserID = sqlb.Col[string](TableActive, ColActiveUserID) ActiveOrgID = sqlb.Col[*string](TableActive, ColActiveOrgID) ActiveTeamID = sqlb.Col[*string](TableActive, ColActiveTeamID) ActiveExpiresAt = sqlb.Col[time.Time](TableActive, ColActiveExpiresAt) IdentityUserID = sqlb.Col[string](TableIdentity, auth.ColIdentityUserID) IdentityKind = sqlb.Col[auth.IdentifierKindName](TableIdentity, auth.ColIdentityKind) IdentityValue = sqlb.Col[string](TableIdentity, auth.ColIdentityValue) IdentityVerifiedAt = sqlb.Col[*time.Time](TableIdentity, auth.ColIdentityVerifiedAt) )
Typed columns, for an app that joins this plugin's tables into its own queries; reads are open, writes go through the operations.
var ( // ErrNotMember answers a non-member with 404, never 403, so a stranger // cannot tell an organization they may not see from one that does not // exist. ErrNotMember = auth.NewError("organization.not_member", http.StatusNotFound, "Not found.") // ErrForbidden is a member whose role is too low for the request. ErrForbidden = auth.NewError("organization.forbidden", http.StatusForbidden, "Your role does not allow this.") // ErrSlugTaken is a slug another organization already uses. ErrSlugTaken = auth.NewError("organization.slug_taken", http.StatusConflict, "This address is already in use.") // ErrBadSlug is a slug that does not match SlugPattern. ErrBadSlug = auth.NewError("organization.bad_slug", http.StatusBadRequest, "Use lowercase letters, digits and dashes.") // ErrBadRole is a role outside RoleValues. ErrBadRole = auth.NewError("organization.bad_role", http.StatusBadRequest, "This role does not exist.") // ErrLastOwner refuses to remove or demote the only owner, which would // leave an organization nobody can administer. ErrLastOwner = auth.NewError("organization.last_owner", http.StatusConflict, "An organization needs at least one owner.") // ErrInviteInvalid is an invitation that is unknown, expired, already // accepted, or addressed to an address the caller has not verified. One // error for all four, so a guessed id reveals nothing. ErrInviteInvalid = auth.NewError("organization.invite_invalid", http.StatusNotFound, "This invitation is not valid any more.") // ErrCreateForbidden is a user the CreateGate refused. ErrCreateForbidden = auth.NewError("organization.create_forbidden", http.StatusForbidden, "You cannot create an organization.") // ErrLimitReached is a user who already owns MaxOwned organizations. ErrLimitReached = auth.NewError("organization.limit_reached", http.StatusConflict, "You have reached the number of organizations you can own.") // ErrMembersLimit is an organization that already has MaxMembers members. ErrMembersLimit = auth.NewError("organization.members_limit", http.StatusConflict, "This organization is full.") // ErrNoActiveOrg is a request that named no organization while its // session has none active. ErrNoActiveOrg = auth.NewError("organization.no_active", http.StatusBadRequest, "No organization is active.") // ErrAlreadyMember refuses to invite or add a member, with // RejectExistingMembers. ErrAlreadyMember = auth.NewError("organization.already_member", http.StatusConflict, "This user is already a member.") // ErrInvitationLimit is an organization with InvitationLimit pending // invitations already. ErrInvitationLimit = auth.NewError("organization.invitation_limit", http.StatusConflict, "This organization has too many pending invitations.") // ErrBadMetadata is metadata that is not a JSON object or exceeds // MaxMetadataSize. ErrBadMetadata = auth.NewError("organization.bad_metadata", http.StatusBadRequest, "Metadata must be a JSON object of at most 16 KiB.") // ErrTeamNotFound is a team that does not exist or that the caller may // not see; one answer for both, like ErrNotMember. ErrTeamNotFound = auth.NewError("organization.team_not_found", http.StatusNotFound, "Not found.") // ErrTeamsLimit is an organization with TeamsConfig.MaxTeams teams. ErrTeamsLimit = auth.NewError("organization.teams_limit", http.StatusConflict, "This organization has reached its number of teams.") // ErrTeamMembersLimit is a team with TeamsConfig.MaxMembersPerTeam // members. ErrTeamMembersLimit = auth.NewError("organization.team_members_limit", http.StatusConflict, "This team is full.") // ErrLastTeam refuses to remove an organization's only team, unless // TeamsConfig.AllowRemovingAllTeams. ErrLastTeam = auth.NewError("organization.last_team", http.StatusConflict, "An organization needs at least one team.") // ErrRoleNotFound is a dynamic role that does not exist in the // organization. ErrRoleNotFound = auth.NewError("organization.role_not_found", http.StatusNotFound, "This role does not exist.") // ErrRoleTaken is a role name the organization, or the static roles, // already use. ErrRoleTaken = auth.NewError("organization.role_taken", http.StatusConflict, "A role with this name already exists.") // ErrRolesLimit is an organization with DynamicRolesConfig.MaxRoles // roles. ErrRolesLimit = auth.NewError("organization.roles_limit", http.StatusConflict, "This organization has reached its number of roles.") // ErrRoleInUse refuses to delete a role members still hold. ErrRoleInUse = auth.NewError("organization.role_in_use", http.StatusConflict, "Members still hold this role.") // ErrBadPermission is a permission outside the access-control // statements, or one the caller does not hold and so cannot grant. ErrBadPermission = auth.NewError("organization.bad_permission", http.StatusBadRequest, "This permission cannot be granted.") // ErrPredefinedRole answers delete-role or update-role naming a role // fixed in configuration (owner, admin, member, or one of // WithRoles'), which only a deploy changes. ErrPredefinedRole = auth.NewError("organization.predefined_role", http.StatusBadRequest, "This role is part of the configuration and cannot be changed here.") // ErrNoActiveTeam answers list-team-members with no teamId when the // session has no active team. ErrNoActiveTeam = auth.NewError("organization.no_active_team", http.StatusBadRequest, "No team is active.") // ErrAlreadyInvited answers invite, with RefuseRepeatInvites, for an // address that already holds a pending invitation to the organization. // The caller holds invitation:create there, so it reveals nothing a // list-invitations does not. ErrAlreadyInvited = auth.NewError("organization.already_invited", http.StatusConflict, "This address already has a pending invitation.") // ErrInviteAddress is an invitee or member named two ways at once // (Email and an Identifier), or by a kind InviteBy does not allow. ErrInviteAddress = auth.NewError("organization.invite_address", http.StatusBadRequest, "Name the person by one address of a kind this app invites by.") )
Errors a client may see. Each carries a stable code for programmatic checks.
var DefaultAccessControl = access.MustNew(DefaultStatements)
DefaultAccessControl is the access control of DefaultStatements.
var DefaultPublicDomains = []string{
"gmail.com", "googlemail.com", "yahoo.com", "yahoo.co.uk", "yahoo.co.in", "ymail.com", "rocketmail.com",
"outlook.com", "hotmail.com", "hotmail.co.uk", "live.com", "msn.com", "icloud.com", "me.com", "mac.com",
"aol.com", "proton.me", "protonmail.com", "pm.me", "gmx.com", "gmx.de", "gmx.net", "web.de", "mail.com",
"yandex.com", "yandex.ru", "mail.ru", "zoho.com", "qq.com", "163.com", "126.com", "fastmail.com",
"hey.com", "tutanota.com", "tuta.io", "rediffmail.com", "naver.com", "hanmail.net", "libero.it", "orange.fr",
}
DefaultPublicDomains are the free mail providers IsDefaultPublicDomain refuses. It is a starting list, not a complete one: an app that serves a region with other providers passes its own DomainsConfig.IsPublicDomain.
var DefaultStatements = access.Statements{ ResourceOrganization: {ActionUpdate, ActionDelete}, ResourceMember: {ActionCreate, ActionUpdate, ActionDelete}, ResourceInvitation: {ActionCreate, ActionCancel}, ResourceTeam: {ActionCreate, ActionUpdate, ActionDelete}, ResourceAC: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, }
DefaultStatements is better-auth's organization vocabulary: what each operation of the plugin checks. An app adding resources of its own builds its access control from a copy of it (WithAccessControl).
var EnrollmentModeValues = []EnrollmentMode{EnrollManual, EnrollInvitation, EnrollSuggestion, EnrollJoin}
EnrollmentModeValues is every EnrollmentMode.
var ErrBadQuery = auth.NewError("organization.bad_query", http.StatusBadRequest, "This sort or filter is not supported.")
ErrBadQuery is a sort key, direction, filter field or operator the members list does not support.
var ErrNoAuthorizer = errors.New("organization: WithAuthorizer needs an authorizer")
ErrNoAuthorizer was WithAuthorizer(nil).
Deprecated: no longer returned. WithAuthorizer(nil) fails auth.New with auth.ErrNilOption naming the option, as every option given nil does.
var FieldNamePattern = auth.KindNamePattern
FieldNamePattern is what an extra column may be named: the core's rule for column extensions, so a name is a plain identifier on every engine.
var FilterOperatorValues = []FilterOperator{OpEq, OpNe, OpIn, OpNotIn, OpContains, OpStartsWith, OpEndsWith, OpLt, OpLte, OpGt, OpGte}
FilterOperatorValues is the canonical list of filter operators.
var InviteStatusValues = []InviteStatus{StatusPending, StatusAccepted, StatusRejected, StatusCanceled}
InviteStatusValues is the canonical list of invitation states.
var MemberFieldValues = []MemberField{FieldRole, FieldUserID, FieldName, FieldJoinedAt, FieldCreatedAt}
MemberFieldValues is the canonical list of filter fields.
var MemberSortValues = []MemberSort{SortJoinedAt, SortCreatedAt, SortName, SortRole, SortUserID}
MemberSortValues is the canonical list of sort keys.
var RoleValues = []Role{RoleOwner, RoleAdmin, RoleMember}
RoleValues is the canonical iteration order, from most to least privileged. Seeds, permission checks and any role selector read from this one list.
var SlugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,47}$`)
SlugPattern is what an organization slug may look like: 2 to 48 lowercase letters, digits and dashes, not starting with a dash. It appears in URLs and subdomains, so it is restricted to what needs no escaping in either, and 48 keeps it inside a 63-character DNS label with room for an app's prefix.
Functions ¶
func DefaultRoles ¶
DefaultRoles returns better-auth's three roles: the owner may do everything; the admin everything but delete the organization; the member only read the roles. A new map each call, so an app can change it.
func EmailDomain ¶
EmailDomain returns the domain of a normalized email address, the part after its last "@", in NormalizeDomain's form; "" when it has none.
func IsDefaultPublicDomain ¶
IsDefaultPublicDomain reports whether domain, normalized, is in DefaultPublicDomains.
func MembersPageQuery ¶
func MembersPageQuery(a *auth.Auth, orgID string, dst *Member, limit, offset int) (sqlb.Query, error)
MembersPageQuery is MembersQuery with a page: limit rows from offset, in the same joined-at order, so the pages of one listing never overlap.
func MembersQuery ¶
MembersQuery lists an organization's members with their core user fields.
This is the canonical plugin-joins-core statement: the membership rows are this plugin's, the email and name are the core's, and both are typed because the plugin imports the core and uses its exported columns rather than hardcoding names.
func MembershipQuery ¶
MembershipQuery resolves one user's role in one organization.
Used by an authorization check, so it returns the role rather than a boolean: the caller compares with Role.AtLeast, which keeps the privilege ordering in one place instead of spreading a chain of equality tests across handlers.
func NormalizeDomain ¶
NormalizeDomain returns the stored form of a domain: trimmed, lowercased, without a leading "@" or a trailing dot. It refuses anything that is not a name of two or more labels with a non-numeric top label, so an IP address or a bare TLD never becomes a domain. Unicode names are given in punycode.
func PendingInvitesQuery ¶
func PendingInvitesQuery(a *auth.Auth, userID string, now time.Time, dst *Invite) (sqlb.Query, error)
PendingInvitesQuery lists the invitations a given user may accept.
Every predicate here is load-bearing and one of them is the whole reason this function exists rather than being written inline at the call site:
The join to auth_identity requires verified_at IS NOT NULL. Without it, a user who has merely TYPED the victim's address into their account settings — and never clicked anything in the victim's inbox — sees and can accept the victim's invitation. That is one missing clause and it is a full workspace takeover. It is also precisely the published CVE in at least one comparable library, which is why the check lives in this one exported function rather than at each call site.
It is the same rule as auth.VerifiedIdentity, written out rather than called: that predicate is bound to the core's alias for auth_identity, and this statement joins the table under the plugin's own alias (TableIdentity, "oid") so it cannot collide with a core join in a composite query. Calling the core predicate here would reference an alias the statement does not have. TestPendingInvitesQuery_RequiresVerifiedIdentity pins the rendered clause.
The address match is exact, on kind and value both (InviteBy): an invitation to a phone number is matched only by a verified phone identity with that number. An identity value is stored normalized, so an invitation's identifier is stored normalized too, through its kind's normalizer at the moment the invitation is written; an exact comparison of two normal forms then needs no lower() in SQL, which would be a second folding rule that differs between engines.
The remaining predicates exclude accepted, rejected, canceled and expired invitations, all of which would otherwise be replayable indefinitely.
func UserBindings ¶
func UserBindings(dst *UserMembership) []sqlb.Binding
UserBindings binds this plugin's composite columns to dst.
Role is coalesced because the LEFT join yields null for a non-member and Role is a named string type that cannot hold it. JoinedAt is bound to a pointer instead, because nil is the honest representation of "never joined" and any coalesced timestamp would be a fabricated date.
func UserJoin ¶
UserJoin brings this user's membership of one specific organization into a composite user query.
Why it takes an argument when the other plugins' joins do not: a user has one phone number but a membership only exists relative to an organization, so the join must be parameterised. The org id is BOUND inside the ON clause rather than interpolated, which is why the resulting statement carries an extra placeholder before the WHERE clause even begins.
LEFT, so a user who is not a member still comes back rather than vanishing from a query that was only asking who they are.
Types ¶
type AcceptRequest ¶
type AcceptRequest struct {
InviteID string `json:"inviteId"`
}
AcceptRequest names an invitation, as the input of accept, reject, cancel and invitation.
type AccessRequest ¶
type AccessRequest struct {
// OrgID is the organization the decision is about.
OrgID string
// UserID is the member whose permission is decided: the caller, or the
// user Can names.
UserID string
// Role is UserID's role in OrgID (Role.Parts for several). A model that
// does not use roles ignores it.
Role Role
// Request is what is asked for, per resource.
Request access.Request
// Connector joins the resources; access.And when empty.
Connector access.Connector
// Operation is the operation asking, or "" outside one (Can,
// RequirePermission, APIKeyPermission).
Operation auth.EndpointID
// Call is the operation's call (client, session, decoded input) for an
// attribute-based rule, or nil outside an operation. Read it; never
// change it.
Call *auth.Call
}
AccessRequest is one permission decision. It is a struct so it can carry more context later without breaking an Authorizer.
type ActiveResult ¶
type ActiveResult struct {
OrgID *string `json:"activeOrganizationId"`
TeamID *string `json:"activeTeamId"`
Org *Org `json:"organization,omitempty"`
}
ActiveResult is the session's selection: the active organization and team ids, nil for none, and the organization itself when one is active.
type AddDomainRequest ¶
type AddDomainRequest struct {
OrgID string `json:"orgId,omitempty"`
Domain string `json:"domain"`
Enrollment EnrollmentMode `json:"enrollmentMode,omitempty"`
Role Role `json:"role,omitempty"`
}
AddDomainRequest is the input of add-domain; an empty OrgID means the active organization. Enrollment is EnrollManual and Role RoleMember when empty.
type AddMemberRequest ¶
AddMemberRequest is AddMemberWith's input: AddMember with the member's extra columns (WithFields).
type AllOrgsRequest ¶
AllOrgsRequest is the input of ListAll. Search keeps organizations whose slug contains it; a slug is lowercase, so the search is lowercased first and matches the same on every engine.
type AllOrgsResult ¶
type AllOrgsResult struct {
Orgs []OrgSummary `json:"orgs"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
AllOrgsResult is one page of ListAll, newest first.
type Authorizer ¶
type Authorizer interface {
Authorize(ctx context.Context, req AccessRequest) (bool, error)
}
Authorizer decides whether an AccessRequest is allowed. Any error refuses: the plugin never reads the boolean of a call that returned an error, so a policy engine that is down fails closed. It must be safe for concurrent use.
type AuthorizerFunc ¶
type AuthorizerFunc func(ctx context.Context, req AccessRequest) (bool, error)
AuthorizerFunc adapts a function to Authorizer.
func (AuthorizerFunc) Authorize ¶
func (f AuthorizerFunc) Authorize(ctx context.Context, req AccessRequest) (bool, error)
Authorize calls f.
type CreateRequest ¶
type CreateRequest struct {
Name string `json:"name"`
Slug string `json:"slug"`
// Logo and Metadata are optional (Org).
Logo *string `json:"logo,omitempty"`
Metadata auth.AppData `json:"metadata,omitempty"`
// KeepActive leaves the session's active organization as it is; by
// default the new organization becomes active, as in better-auth
// (keepCurrentActiveOrganization).
KeepActive bool `json:"keepCurrentActiveOrganization,omitempty"`
// Fields set the organization's extra columns (WithFields).
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
CreateRequest is the input of create.
func (*CreateRequest) UnmarshalJSON ¶
func (r *CreateRequest) UnmarshalJSON(data []byte) error
UnmarshalJSON keeps top-level field keys for create.
type CreateRoleRequest ¶
type CreateRoleRequest struct {
OrgID string `json:"orgId,omitempty"`
Role Role `json:"role"`
Permission access.Permissions `json:"permission"`
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
CreateRoleRequest is the input of create-role; an empty OrgID means the active organization.
func (CreateRoleRequest) AuditOrgID ¶
func (r CreateRoleRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for create-role.
func (*CreateRoleRequest) UnmarshalJSON ¶
func (r *CreateRoleRequest) UnmarshalJSON(data []byte) error
UnmarshalJSON keeps top-level field keys for create-role.
type CreateTeamRequest ¶
type CreateTeamRequest struct {
OrgID string `json:"orgId,omitempty"`
Name string `json:"name"`
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
CreateTeamRequest is the input of create-team; an empty OrgID means the active organization.
func (CreateTeamRequest) AuditOrgID ¶
func (r CreateTeamRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for create-team.
func (*CreateTeamRequest) UnmarshalJSON ¶
func (r *CreateTeamRequest) UnmarshalJSON(data []byte) error
UnmarshalJSON keeps top-level field keys for create-team.
type DNSVerifier ¶
type DNSVerifier struct {
Resolver TXTResolver
}
DNSVerifier is the default DomainVerifier: the challenge's TXT record must hold its value exactly. Resolver is net.DefaultResolver when nil.
func (DNSVerifier) VerifyDomain ¶
func (v DNSVerifier) VerifyDomain(ctx context.Context, ch DomainChallenge) (bool, error)
VerifyDomain looks the record up. A name that does not exist is "not yet", not an error, since that is the state before the admin publishes it.
type DomainChallenge ¶
DomainChallenge is what a DomainVerifier checks: that RecordName has a TXT record whose value is RecordValue.
type DomainRequest ¶
type DomainRequest struct {
DomainID string `json:"domainId"`
}
DomainRequest names a domain, as the input of verify-domain, remove-domain and accept-suggestion.
type DomainSuggestion ¶
type DomainSuggestion struct {
DomainID string `json:"domainId"`
OrgID string `json:"orgId"`
OrgName string `json:"orgName"`
OrgSlug string `json:"orgSlug"`
Domain string `json:"domain"`
// Requested is true once the caller asked to join and an admin has not
// decided yet.
Requested bool `json:"requested"`
}
DomainSuggestion is an organization the caller may ask to join, because it verified the domain of one of the caller's verified emails.
type DomainVerifier ¶
type DomainVerifier interface {
VerifyDomain(ctx context.Context, ch DomainChallenge) (bool, error)
}
DomainVerifier decides whether an organization controls a domain. The default is DNSVerifier; an app replaces it (DomainsConfig.Verifier) to check an HTTP file, ask its registrar's API, or use a fake in tests. It answers false with no error when the proof is not there yet, and an error only when it could not look. ds:def id=domainverifier-xyngwy9v owner=@khanakia stability=api
type DomainVerifierFunc ¶
type DomainVerifierFunc func(ctx context.Context, ch DomainChallenge) (bool, error)
DomainVerifierFunc adapts a function to DomainVerifier.
func (DomainVerifierFunc) VerifyDomain ¶
func (f DomainVerifierFunc) VerifyDomain(ctx context.Context, ch DomainChallenge) (bool, error)
VerifyDomain calls f.
type DomainsConfig ¶
type DomainsConfig struct {
// Verifier proves ownership; DNSVerifier{} when nil.
Verifier DomainVerifier
// RecordPrefix is the TXT record's label, DefaultDomainRecordPrefix when
// empty. It must start with an underscore.
RecordPrefix string
// TrustVerifiedEmail verifies a domain without DNS when the admin who
// adds or verifies it holds a verified email at exactly that domain.
// Off by default: it lets any employee with a work address claim the
// company's domain for an organization they created, which DNS does not.
TrustVerifiedEmail bool
// IsPublicDomain reports a domain that no organization may add, because
// anyone can get an address there; IsDefaultPublicDomain when nil.
IsPublicDomain func(domain string) bool
// MaxDomains caps the domains of one organization; DefaultMaxDomains
// when zero. Negative is refused.
MaxDomains int
}
DomainsConfig configures verified domains (Domains). Every field is optional.
type DomainsRequest ¶
type DomainsRequest struct {
OrgID string `json:"orgId,omitempty"`
}
DomainsRequest names an organization, as the input of list-domains and list-membership-requests; empty means the active one.
type DomainsResult ¶
type DomainsResult struct {
Domains []OrgDomain `json:"domains"`
}
DomainsResult is the output of list-domains.
type DynamicRolesConfig ¶
type DynamicRolesConfig struct {
// MaxRoles caps the dynamic roles of one organization
// (maximumRolesPerOrganization); zero is no cap.
MaxRoles int
}
DynamicRolesConfig configures roles created at run time (DynamicRoles), better-auth's dynamicAccessControl.
type EnrollmentMode ¶
type EnrollmentMode string
EnrollmentMode is what a verified domain does for a user whose email at the domain is verified. The names are Clerk's, plus automatic_join. ds:def id=enrollmentmode-3c3jpwcb owner=@khanakia stability=stable
const ( // EnrollManual does nothing automatically: members are invited by hand. // The default, so adding a domain changes nobody's access by itself. EnrollManual EnrollmentMode = "manual_invitation" // EnrollInvitation creates a pending invitation (organization.invites), // which the user accepts as any other. EnrollInvitation EnrollmentMode = "automatic_invitation" // EnrollSuggestion lists the organization among the user's // domain-suggestions; accepting one asks to join, and an admin approves. EnrollSuggestion EnrollmentMode = "automatic_suggestion" // EnrollJoin makes the user a member with the domain's role. EnrollJoin EnrollmentMode = "automatic_join" )
Enrollment modes.
type Field ¶
type Field struct {
// Name is the column name; it must match FieldNamePattern and must not
// be one of the table's own columns.
Name string
// Type is one of auth.ColumnTypeValues; a value is decoded as the core
// decodes sign-up fields (auth.Fields): a JSON string for text, an
// integer for int, true or false for bool, an RFC 3339 string for
// timestamp, null for a nullable column.
Type auth.ColumnType
Nullable bool
// Default is a constant SQL literal ('x', 42, true, false) or empty. A
// NOT NULL field needs one, so existing rows can be filled.
Default string
// Required refuses a create that does not give the field
// (better-auth's required: true).
Required bool
// ServerOnly refuses the field in a request from a client: only Go
// calls and hooks set it (better-auth's input: false). For a value the
// app decides, such as a plan or an external id.
ServerOnly bool
}
Field is one extra column on one of the plugin's tables.
type FieldsConfig ¶
type FieldsConfig struct {
Org, Member, Invitation, Team, Role []Field
}
FieldsConfig lists the extra columns per table (WithFields). Team fields need Teams and role fields DynamicRoles; Validate refuses them otherwise.
type FilterOperator ¶
type FilterOperator string
FilterOperator is how a members filter compares.
const ( OpEq FilterOperator = "eq" OpNe FilterOperator = "ne" OpIn FilterOperator = "in" OpNotIn FilterOperator = "not_in" OpContains FilterOperator = "contains" OpStartsWith FilterOperator = "starts_with" OpEndsWith FilterOperator = "ends_with" OpLt FilterOperator = "lt" OpLte FilterOperator = "lte" OpGt FilterOperator = "gt" OpGte FilterOperator = "gte" )
Filter operators, better-auth's names. A text field (role, userId, name) takes eq, ne, in, not_in, contains, starts_with and ends_with, all case-sensitive on every engine (memberTextMatch); the time field takes eq, ne, lt, lte, gt and gte. The order operators are refused on text, because "less than" between two names depends on the engine's collation, so the same request would answer differently on Postgres and SQLite.
type FullOrg ¶
type FullOrg struct {
Org
Members []MemberView `json:"members"`
Invitations []Invitation `json:"invitations"`
Teams []Team `json:"teams,omitempty"`
}
FullOrg is an organization with its members, its pending invitations and, with Teams, its teams: one read for a settings page.
type GetRequest ¶
type GetRequest struct {
OrgID string `json:"orgId,omitempty"`
OrgSlug string `json:"orgSlug,omitempty"`
MembersLimit int `json:"membersLimit,omitempty,string"`
}
GetRequest names an organization by id or slug, the active one when both are empty, as the input of get-organization and get-full-organization. MembersLimit bounds the members of the full read (DefaultMembersPage, at most MaxMembersPage).
func (GetRequest) AuditOrgID ¶
func (r GetRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for get and get-full.
type Hooks ¶
type Hooks struct {
BeforeCreateOrg func(ctx context.Context, org *Org, creator auth.User) error
AfterCreateOrg func(ctx context.Context, org Org, creator auth.User) error
BeforeUpdateOrg func(ctx context.Context, old Org, org *Org) error
AfterUpdateOrg func(ctx context.Context, org Org) error
BeforeDeleteOrg func(ctx context.Context, org Org) error
AfterDeleteOrg func(ctx context.Context, org Org) error
// BeforeAddMember runs when someone joins (an accepted invitation,
// AddMember), not for the creator, who joins in BeforeCreateOrg's
// operation.
BeforeAddMember func(ctx context.Context, m *MemberChange) error
AfterAddMember func(ctx context.Context, m MemberChange) error
BeforeRemoveMember func(ctx context.Context, m MemberChange) error
AfterRemoveMember func(ctx context.Context, m MemberChange) error
BeforeUpdateMemberRole func(ctx context.Context, m *MemberChange, old Role) error
AfterUpdateMemberRole func(ctx context.Context, m MemberChange, old Role) error
// BeforeCreateInvitation may change the role, team and expiry.
BeforeCreateInvitation func(ctx context.Context, inv *Invitation, inviter auth.User) error
AfterCreateInvitation func(ctx context.Context, inv Invitation, inviter auth.User) error
BeforeAcceptInvitation func(ctx context.Context, inv Invitation, user auth.User) error
AfterAcceptInvitation func(ctx context.Context, inv Invitation, user auth.User) error
BeforeRejectInvitation func(ctx context.Context, inv Invitation, user auth.User) error
AfterRejectInvitation func(ctx context.Context, inv Invitation, user auth.User) error
BeforeCancelInvitation func(ctx context.Context, inv Invitation, by auth.User) error
AfterCancelInvitation func(ctx context.Context, inv Invitation, by auth.User) error
BeforeCreateTeam func(ctx context.Context, team *Team) error
AfterCreateTeam func(ctx context.Context, team Team) error
BeforeUpdateTeam func(ctx context.Context, old Team, team *Team) error
AfterUpdateTeam func(ctx context.Context, team Team) error
BeforeDeleteTeam func(ctx context.Context, team Team) error
AfterDeleteTeam func(ctx context.Context, team Team) error
BeforeAddTeamMember func(ctx context.Context, m TeamMemberChange) error
AfterAddTeamMember func(ctx context.Context, m TeamMemberChange) error
BeforeRemoveTeamMember func(ctx context.Context, m TeamMemberChange) error
AfterRemoveTeamMember func(ctx context.Context, m TeamMemberChange) error
}
Hooks are the organization lifecycle hooks, better-auth's organizationHooks: every one is optional and runs inside the operation's transaction. A Before hook may change the row it is given (a pointer) or refuse with an error, which becomes the operation's error; an After hook's error fails the operation and rolls everything back.
What a Before hook changes is checked again: a slug or a role it sets must pass the same rules as the request's, and the fields the checks were made for (ids, the invited address) are restored, so a hook can shape a row but never move it past an authorization check.
A hook writes the app's own rows in the same transaction through the *auth.Auth: a.Conn(ctx) is that transaction, so pass WithHooksFrom, whose builder receives a, rather than WithHooks. The same ctx is the one to pass to any other library call the hook makes, which then joins it too.
auth.OperationHook still works on every operation id; these hooks add the typed row and the power to change it.
type Invitation ¶
type Invitation struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
// Email is the invited address of an email invitation, "" for another
// kind; Kind and Identifier address every invitation.
Email string `json:"email"`
Kind auth.IdentifierKindName `json:"kind"`
Identifier string `json:"identifier"`
Role Role `json:"role"`
Status InviteStatus `json:"status"`
InviterID string `json:"inviterId"`
TeamID *string `json:"teamId,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
// Fields are the extra columns WithFields declares.
Fields auth.Fields `json:"fields,omitempty"`
}
Invitation is an invitation row as an organization's members see it: every state, who sent it, and the team it joins.
type InvitationData ¶
type InvitationData struct {
InvitationID string `json:"invitationId"`
// Email is the invited address of an email invitation, "" for another
// kind; Kind and Identifier address every invitation.
Email string `json:"email"`
Kind auth.IdentifierKindName `json:"kind"`
Identifier string `json:"identifier"`
Role Role `json:"role"`
TeamID *string `json:"teamId,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
OrganizationID string `json:"organizationId"`
OrganizationName string `json:"organizationName"`
OrganizationSlug string `json:"organizationSlug"`
OrganizationLogo *string `json:"organizationLogo,omitempty"`
InviterID string `json:"inviterId"`
InviterName string `json:"inviterName"`
InviterEmail string `json:"inviterEmail,omitempty"`
}
InvitationData is the typed payload of every PurposeInvite message (auth.Message.Data): what better-auth hands its sendInvitationEmail (invitation id, role, invited address, the organization, the inviter), for an app template to read with
d, ok := m.Data.(organization.InvitationData)
instead of parsing the default wording. The link is Message.Link. Through auth.WithDeliveryOutbox the value comes back as this type, restored by the plugin's DecodeMessageData.
InviterEmail is the inviter's primary verified address, "" when they have none: the invitee is told who invited them, as better-auth's template is.
type InvitationsResult ¶
type InvitationsResult struct {
Invitations []Invitation `json:"invitations"`
}
InvitationsResult is the output of list-invitations.
type Invite ¶
type Invite struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
OrgName string `json:"orgName"`
// Email is the invited address of an email invitation, "" for another
// kind; Kind and Identifier address every invitation.
Email string `json:"email"`
Kind auth.IdentifierKindName `json:"kind"`
Identifier string `json:"identifier"`
Role Role `json:"role"`
ExpiresAt time.Time `json:"expiresAt"`
}
Invite is a pending invitation as its invitee sees it: OrgName is joined in so an invitations page needs no second query.
type InviteMail ¶
type InviteMail struct {
Invite Invite
Org Org
Inviter auth.User
// Link is the invitations page, from InviteLink or the base URL.
Link string
}
InviteMail is what InviteMessage receives.
type InviteRequest ¶
type InviteRequest struct {
OrgID string `json:"orgId,omitempty"`
Email string `json:"email,omitempty"`
Kind auth.IdentifierKindName `json:"kind,omitempty"`
Identifier string `json:"identifier,omitempty"`
Role Role `json:"role"`
TeamID string `json:"teamId,omitempty"`
// Resend, when the address already holds a pending invitation to the
// organization, sends that invitation again with a fresh expiry instead
// of creating another (better-auth's resend). The existing invitation
// keeps its id, role and team; Role must still be one the caller may
// invite with. With no pending invitation it is an ordinary invite.
Resend bool `json:"resend,omitempty"`
// Fields set the invitation's extra columns (WithFields).
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
InviteRequest is the input of invite. The invitee is Email (better-auth's field) or, for any kind InviteBy allows, Kind and Identifier; a Kind left empty is InviteBy's first kind. Naming both Email and an Identifier, or a kind InviteBy does not allow, is ErrInviteAddress. The value is normalized by its kind before it is stored; an empty OrgID means the session's active organization. Role may hold several roles (a JSON array or a comma-joined string), and only a holder of the creator role may invite with it. TeamID, with Teams on, also joins the invitee to that team of the organization on acceptance.
func (InviteRequest) AuditOrgID ¶
func (r InviteRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for invite.
func (*InviteRequest) UnmarshalJSON ¶
func (r *InviteRequest) UnmarshalJSON(data []byte) error
UnmarshalJSON keeps top-level field keys for invite.
type InviteStatus ¶
type InviteStatus string
InviteStatus is where an invitation is in its life. Stored on the row, so an admin's list shows what became of every invitation.
const ( StatusPending InviteStatus = "pending" StatusAccepted InviteStatus = "accepted" StatusRejected InviteStatus = "rejected" StatusCanceled InviteStatus = "canceled" )
Invitation states, as better-auth names them.
type InvitesResult ¶
type InvitesResult struct {
Invites []Invite `json:"invites"`
}
InvitesResult is the output of invites.
type ListResult ¶
type ListResult struct {
Orgs []OrgWithRole `json:"orgs"`
}
ListResult is the output of list.
type Member ¶
Member is a user plus their membership, as MembersQuery scans it. The user is embedded because its columns live on auth_user itself (ADR-0001).
type MemberChange ¶
type MemberChange struct {
OrgID string
UserID string
Role Role
// Fields are the member's extra columns on add; a BeforeAddMember hook
// may set them, ServerOnly ones included.
Fields auth.Fields
}
MemberChange is a membership a member hook sees: who, where, and the role they get (add, update) or hold (remove).
type MemberField ¶
type MemberField string
MemberField is what a members filter compares.
const ( FieldRole MemberField = "role" FieldUserID MemberField = "userId" FieldName MemberField = "name" FieldJoinedAt MemberField = "joinedAt" FieldCreatedAt MemberField = "createdAt" )
Member filter fields. JoinedAt and CreatedAt are the same column, when the member joined, under our name and better-auth's.
type MemberRequest ¶
type MemberRequest struct {
OrgID string `json:"orgId,omitempty"`
UserID string `json:"userId,omitempty"`
// Email names the member by a verified address instead of UserID, for a
// settings page that lists addresses. An address nobody verified is
// `organization.not_member`, the same as an unknown user id. Kind and
// Identifier do the same for any identifier kind, such as a verified
// phone number; naming both Email and an Identifier is ErrInviteAddress.
Email string `json:"email,omitempty"`
Kind auth.IdentifierKindName `json:"kind,omitempty"`
Identifier string `json:"identifier,omitempty"`
}
MemberRequest names a member of an organization, as the input of remove-member.
func (MemberRequest) AuditOrgID ¶
func (r MemberRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for remove-member.
func (MemberRequest) TargetUserID ¶
func (r MemberRequest) TargetUserID() string
TargetUserID names the member a request is about, for plugin/audit's Targeted (an event's subjectId), so the member sees it in their own history.
type MemberSort ¶
type MemberSort string
MemberSort is what the members list is ordered by. A closed set rather than a column name, so a request can never order by, and so probe, a column it should not read.
const ( // SortJoinedAt orders by when the member joined; the default. SortJoinedAt MemberSort = "joinedAt" // SortCreatedAt is better-auth's name for the same order. SortCreatedAt MemberSort = "createdAt" SortName MemberSort = "name" SortRole MemberSort = "role" SortUserID MemberSort = "userId" )
Member sort keys.
type MemberView ¶
type MemberView struct {
User auth.UserView `json:"user"`
Role Role `json:"role"`
JoinedAt time.Time `json:"joinedAt"`
// Fields are the member's extra columns (WithFields).
Fields auth.Fields `json:"fields,omitempty"`
}
MemberView is one row of members, with the user reduced to the fields safe to show another member.
type MembersResult ¶
type MembersResult struct {
Members []MemberView `json:"members"`
// Total counts every member; Page and Limit are the values used.
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
MembersResult is the output of members.
type MembershipRequest ¶
type MembershipRequest struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
DomainID string `json:"domainId"`
Domain string `json:"domain"`
User auth.UserView `json:"user"`
Status RequestStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
}
MembershipRequest is a user's request to join, made from a suggestion.
type MembershipRequestsResult ¶
type MembershipRequestsResult struct {
Requests []MembershipRequest `json:"requests"`
}
MembershipRequestsResult is the output of list-membership-requests.
type Option ¶
type Option func(*Plugin)
Option configures the plugin; pass options to New.
func CreateGate ¶
CreateGate decides whether a signed-in user may create an organization, for example only on a paid plan. A refusal is `organization.create_forbidden`; an error from fn fails the request. Without it every signed-in user may.
func CreatorRole ¶
CreatorRole sets the role the creator of an organization gets (better-auth's creatorRole). It is also the role the owner protections guard: only its holders may grant it, change or remove a holder, and the last holder cannot leave or be demoted. RoleOwner by default; Validate refuses a role that is not a static role.
func Domains ¶
func Domains(cfg DomainsConfig) Option
Domains turns verified domains on: the domain operations, their tables and the enrollment of users whose verified email is at a verified domain.
func DynamicRoles ¶
func DynamicRoles(cfg DynamicRolesConfig) Option
DynamicRoles lets organizations create roles at run time, each a permission map stored in auth_org_role.
func InvitationLimit ¶
InvitationLimit caps the pending invitations of one organization (better-auth's invitationLimit); one more is `organization.invitation_limit`. DefaultInvitationLimit unless set; zero is no cap. It complements InviteLimit, which bounds mail per client address over time.
func InviteBy ¶
func InviteBy(kinds ...auth.IdentifierKindName) Option
InviteBy sets the identifier kinds an invitation may be addressed to, in place of email alone: InviteBy(phone.KindPhone) for a phone-only app, InviteBy(email.KindEmail, phone.KindPhone) for either. The first kind is the one an InviteRequest naming no Kind uses. Each kind must be registered by an identifier plugin that delivers messages (email and phone do; username does not): auth.New refuses any other kind with auth.ErrNoDeliverer (auth.Config.CanDeliver), rather than the first invite failing. Whatever the kind, an invitation is accepted only by a user holding the VERIFIED identity of that kind and value (PendingInvitesQuery). The email plugin is required only when email is among the kinds.
func InviteLimit ¶
InviteLimit replaces DefaultInviteLimit and DefaultInviteWindow: at most limit invitations per window from one client address. Both must be positive; Validate refuses anything else.
func InviteLink ¶
InviteLink sets the page an invitation mail links to, for an app whose invitations page is not BaseURL + DefaultInvitePage. It is called per mail, so it may read configuration that changes at run time.
func InviteMessage ¶
func InviteMessage(fn func(m InviteMail) (subject, text string)) Option
InviteMessage words the invitation mail from the invitation, the organization, the inviter and the link, for an app that wants "Alice invited you to Acme" rather than the plugin's generic text. It sets the message's default wording, so an email.Templates case for PurposeInvite still wins.
func MaxMembers ¶
MaxMembers caps the members of an organization; an acceptance or AddMember that would exceed it is refused with `organization.members_limit`. Invitations are still sent, because the count is checked when someone joins. Zero, the default, is no cap.
func MaxOwned ¶
MaxOwned caps how many organizations one user may own; creating another is refused with `organization.limit_reached`. Counted as memberships holding the creator role, so an owner who was promoted counts and one who left does not. Zero, the default, is no cap.
func RefuseRepeatInvites ¶
func RefuseRepeatInvites() Option
RefuseRepeatInvites makes inviting an address that already holds a pending, unexpired invitation to the organization fail with `organization.already_invited` (better-auth's default), unless the request sets Resend. ReplacePendingInvites wins over it, since it asks for the opposite. Without either, the default, each invite creates another pending invitation.
func RejectExistingMembers ¶
func RejectExistingMembers() Option
RejectExistingMembers refuses to invite an address that already belongs to a member, and AddMember of a member, with `organization.already_member`, as better-auth does. Without it, the default, the invitation is sent and accepting it keeps the member's role.
func ReplacePendingInvites ¶
func ReplacePendingInvites() Option
ReplacePendingInvites makes inviting an address that already has pending invitations to the same organization withdraw them first, so the invitee has one valid invitation and one fresh mail. Without it, the default, each invite creates another pending invitation.
func Teams ¶
func Teams(cfg TeamsConfig) Option
Teams turns teams on: sub-groups inside an organization, with their own members and an active team per session.
func WithAccessControl ¶
func WithAccessControl(ac *access.AccessControl) Option
WithAccessControl replaces DefaultStatements with the app's own vocabulary. The plugin's operations check the resources of DefaultStatements (organization, member, invitation, team, ac), so an app adding its own resources should start from DefaultStatements, as better-auth recommends.
func WithAuthorizer ¶
func WithAuthorizer(a Authorizer) Option
WithAuthorizer replaces the role check as the decider of every organization permission. nil is refused by auth.New. An authorizer that only adds a rule wraps p.RoleAuthorizer(). The creator role's all-permissions rule for API keys belongs to the role model, so with a custom authorizer it decides organization keys too. Roles stay the plugin's for everything that is not a permission: which roles a member may be given, and the owner protections.
func WithFields ¶
func WithFields(cfg FieldsConfig) Option
WithFields declares extra columns on the plugin's tables.
func WithHooks ¶
WithHooks sets the lifecycle hooks; see Hooks. For hooks that write the app's own rows, which need the *auth.Auth, use WithHooksFrom.
func WithHooksFrom ¶
WithHooksFrom sets the lifecycle hooks from build, which Init calls once with the *auth.Auth: the options are built before auth.New returns it, so this is how a hook gets it without a variable bound after start-up. Every hook runs inside its operation's transaction and ctx carries it, so a hook writes the app's own rows through a.Conn(ctx) or a.Exec(ctx, …) and they commit or roll back with the organization's. Giving WithHooks as well is refused by auth.New.
func WithInviteTTL ¶
WithInviteTTL overrides DefaultInviteTTL, for a product whose invitations should lapse sooner or later than a week.
func WithRoles ¶
WithRoles adds static roles, or replaces a built-in one under the same name, each a role of the access control (WithAccessControl or the default). Validate refuses a role naming a statement the access control does not declare.
Example ¶
ExampleWithRoles adds an app resource and a role of its own beside the built-in ones. ds:def id=examplewithroles-unstje3r owner=@khanakia stability=stable
package main
import (
"fmt"
"github.com/ubgo/auth/plugin/access"
"github.com/ubgo/auth/plugin/organization"
)
func main() {
s := access.Statements{"project": {"create", "share"}}
for k, v := range organization.DefaultStatements {
s[k] = v
}
ac := access.MustNew(s)
billing := ac.MustRole(access.Permissions{"project": {"create"}, organization.ResourceInvitation: {organization.ActionCreate}})
org := organization.New(organization.WithAccessControl(ac), organization.WithRoles(map[organization.Role]access.Role{"billing": billing}))
fmt.Println(org.Name(), billing.Allows(access.Request{"project": access.All("create")}))
}
Output: organization true
func WithoutInvitations ¶
func WithoutInvitations() Option
WithoutInvitations turns the invitation operations off (invite, invites, accept, invitation, reject-invitation, cancel-invitation, list-invitations), for an app that adds members only from the server (AddMember, AddMemberWith). No identifier plugin, invitation link or BaseURL is then required.
type Org ¶
type Org struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
Logo *string `json:"logo,omitempty"`
Metadata auth.AppData `json:"metadata,omitempty"`
CreatedAt time.Time `json:"createdAt"`
// Fields are the extra columns WithFields declares.
Fields auth.Fields `json:"fields,omitempty"`
}
Org is a workspace. Slug is unique across the deployment and normalized to lowercase at creation. Logo and Metadata are optional: a logo URL, and free-form JSON the app keeps on the organization (at most MaxMetadataSize bytes).
type OrgDomain ¶
type OrgDomain struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
Domain string `json:"domain"`
Enrollment EnrollmentMode `json:"enrollmentMode"`
Role Role `json:"role"`
Verified bool `json:"verified"`
VerifiedAt *time.Time `json:"verifiedAt,omitempty"`
RecordName string `json:"recordName"`
RecordValue string `json:"recordValue"`
CreatedAt time.Time `json:"createdAt"`
// contains filtered or unexported fields
}
OrgDomain is a domain of an organization. RecordName and RecordValue are the TXT record to publish; they stay meaningful after verification, which keeps a later re-check possible.
type OrgRequest ¶
type OrgRequest struct {
OrgID string `json:"orgId,omitempty"`
Page int `json:"page,omitempty,string"`
Limit int `json:"limit,omitempty,string"`
// SortBy and SortDirection order the members list (MemberSort,
// DefaultMemberSort ascending by default).
SortBy MemberSort `json:"sortBy,omitempty"`
SortDirection Direction `json:"sortDirection,omitempty"`
// FilterField, FilterOperator and FilterValue narrow the members list:
// in and not_in compare with each of the comma-separated values, and a
// joinedAt/createdAt value is RFC 3339 (memberFilter).
FilterField MemberField `json:"filterField,omitempty"`
FilterOperator FilterOperator `json:"filterOperator,omitempty"`
FilterValue string `json:"filterValue,omitempty"`
}
OrgRequest names an organization, as the input of members, role, delete and the list operations; an empty OrgID means the session's active organization. Page and Limit page the members list (strings in JSON because a Query's input arrives as URL values); Sort and Filter order and narrow it. All but OrgID are optional.
func (OrgRequest) AuditOrgID ¶
func (r OrgRequest) AuditOrgID() string
AuditOrgID names the organization a request acts in, for plugin/audit's OrgScoped (an event's orgId), without this package importing it. Empty when the request leaves the organization to the session's active one; the operations then annotate the call with the organization they resolved.
type OrgRole ¶
type OrgRole struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
Role Role `json:"role"`
Permission access.Permissions `json:"permission"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// Fields are the extra columns WithFields declares
// (organizationRole.additionalFields).
Fields auth.Fields `json:"fields,omitempty"`
}
OrgRole is a role an organization created at run time: a name and the permissions it grants, from the access control's statements.
type OrgSummary ¶
OrgSummary is an organization with how many members it has.
type OrgUpdate ¶
type OrgUpdate struct {
Name *string `json:"name,omitempty"`
Slug *string `json:"slug,omitempty"`
Logo *string `json:"logo,omitempty"`
Metadata auth.AppData `json:"metadata,omitempty"`
// Fields change the extra columns named; the others stay.
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
OrgUpdate is what update changes; a nil field stays as it is. An empty Logo clears the logo; Metadata replaces the whole document.
func (*OrgUpdate) UnmarshalJSON ¶
UnmarshalJSON keeps top-level field keys inside update's data.
type OrgWithRole ¶
OrgWithRole is one organization and the caller's role in it.
type PermissionDoc ¶
type PermissionDoc access.Permissions
PermissionDoc is access.Permissions as a column: JSON text on every engine.
func (*PermissionDoc) Scan ¶
func (d *PermissionDoc) Scan(src any) error
Scan reads the JSON text the three drivers return as a string or bytes.
type PermissionRequest ¶
type PermissionRequest struct {
OrgID string `json:"orgId,omitempty"`
Permissions access.Permissions `json:"permissions"`
Connector access.Connector `json:"connector,omitempty"`
}
PermissionRequest is the input of has-permission: the permissions asked for, in an organization (the active one when OrgID is empty). Connector joins the resources, AND by default.
func (PermissionRequest) AuditOrgID ¶
func (r PermissionRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for has-permission.
type PermissionResult ¶
type PermissionResult struct {
Success bool `json:"success"`
}
PermissionResult is the output of has-permission.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin adds organizations. Build it with New; the zero value has no operations.
func New ¶
New builds the plugin with DefaultInviteTTL and the invitation page at BaseURL + DefaultInvitePage unless opts say otherwise.
func (*Plugin) APIKeyPermission ¶
func (p *Plugin) APIKeyPermission(ctx context.Context, orgID, userID, action string) (member, allowed bool, err error)
APIKeyPermission answers plugin/apikey's question about an organization key (better-auth's checkOrgApiKeyPermission), so *Plugin satisfies apikey.Organizations without either module importing the other: member is false for someone outside orgID; a holder of the creator role may do every action (allowCreatorAllPermissions) under the default RoleAuthorizer; anyone else, and everyone under WithAuthorizer, needs the authorizer to allow action on the "apiKey" resource. Only ErrNotMember is "not a member"; any other failure is returned.
func (*Plugin) AcceptSuggestion ¶
AcceptSuggestion asks to join the organization of a suggested domain; an admin approves the request.
func (*Plugin) Active ¶
func (p *Plugin) Active(ctx context.Context) (ActiveResult, error)
Active returns the signed-in session's selection.
func (*Plugin) ActiveOrgID ¶
ActiveOrgID returns the active organization of the request's session in ctx, from Go, for an app route that scopes to it. ok is false for none, a signed-out request and an API-key request. It resolves the session first, so it works outside an operation and outside authhttp.Middleware.
func (*Plugin) AddDomain ¶
AddDomain adds a domain to an organization, as a member with organization:update. It is unverified until VerifyDomain, unless TrustVerifiedEmail and the caller's verified email prove it at once.
func (*Plugin) AddMember ¶
AddMember adds a user to an organization with role, from Go only, without an invitation: an import, or a product that joins users to a workspace by rule. Someone already in the organization keeps their role, or is refused with RejectExistingMembers. MaxMembers and the member hooks apply. It runs in one transaction (joining the caller's, if any), like CreateFor.
func (*Plugin) AddMemberWith ¶
func (p *Plugin) AddMemberWith(ctx context.Context, in AddMemberRequest) error
AddMemberWith is AddMember with member fields, from Go only (better-auth's server-side addMember with additional fields); ServerOnly fields are allowed, since the caller is trusted code.
func (*Plugin) AddTeamMember ¶
func (p *Plugin) AddTeamMember(ctx context.Context, in TeamMemberRequest) error
AddTeamMember adds a member of the organization to a team, as a member with team:update.
func (*Plugin) ApproveMembershipRequest ¶
ApproveMembershipRequest makes the requester a member with the domain's role, as a member with member:create.
func (*Plugin) Can ¶
func (p *Plugin) Can(ctx context.Context, orgID, userID string, req access.Request, connector access.Connector) (bool, error)
Can reports whether userID's roles in orgID pass req under connector (AND when empty), from Go: the server-side check behind an app's own routes. A non-member is ErrNotMember.
func (*Plugin) Cancel ¶
Cancel withdraws a pending invitation, as a member of its organization with invitation:cancel.
func (*Plugin) CancelFor ¶
CancelFor withdraws a pending invitation of any organization. by is recorded as who canceled it and handed to the cancel hooks; an invitation that is unknown or no longer pending is ErrInviteInvalid.
func (*Plugin) CreateFor ¶
CreateFor makes an organization owned by userID, from Go only: a seed, an import, a sign-up flow that creates the user's workspace. The CreateGate and MaxOwned do not apply, because the caller is trusted code, not a request; the slug rules and the hooks do. No session's active organization changes. It runs in one transaction (joining the caller's, if any), so a failing hook leaves nothing behind.
func (*Plugin) CreateRole ¶
CreateRole creates a dynamic role, as a member with ac:create who holds every permission the role grants.
func (*Plugin) CreateTeam ¶
CreateTeam creates a team, as a member with team:create.
func (*Plugin) DataHooks ¶
DataHooks enroll a user when one of their emails becomes verified: an identity created verified (a sign-up through a provider that vouches for the address) or one whose verified_at is set. Nothing else enrolls, so an unverified address never does. Off unless Domains is on.
func (*Plugin) DecodeMessageData ¶
DecodeMessageData restores an InvitationData the delivery outbox stored as JSON. Any other purpose is not this plugin's: (nil, nil).
func (*Plugin) Delete ¶
Delete deletes an organization and everything in it, as a member with organization:delete.
func (*Plugin) DeleteFor ¶
DeleteFor deletes any organization and every row of it, between the delete hooks.
func (*Plugin) DeleteRole ¶
func (p *Plugin) DeleteRole(ctx context.Context, in RoleRequest) error
DeleteRole deletes a dynamic role no member holds, as a member with ac:delete.
func (*Plugin) DomainSuggestions ¶
func (p *Plugin) DomainSuggestions(ctx context.Context) ([]DomainSuggestion, error)
DomainSuggestions lists the organizations the signed-in user may ask to join through a verified domain in EnrollSuggestion mode.
func (*Plugin) GetFor ¶
GetFor reads any organization by id; ErrNotMember when there is none, the code every other read gives for an unknown organization.
func (*Plugin) GetFull ¶
GetFull returns an organization with its members, pending invitations and teams.
func (*Plugin) HasPermission ¶
HasPermission reports whether the signed-in member holds the permissions (better-auth's hasPermission). A non-member is ErrNotMember.
func (*Plugin) Init ¶
Init captures the core handle, builds WithHooksFrom's hooks and, when no InviteLink was given, fixes the invitation link to BaseURL + DefaultInvitePage.
func (*Plugin) Invitation ¶
Invitation returns one of the signed-in user's pending invitations by id, for an accept page; ErrInviteInvalid for any other id.
func (*Plugin) InvitationsFor ¶
InvitationsFor lists every invitation of any organization, newest first, with its status, as list-invitations does for a member.
func (*Plugin) Invite ¶
func (p *Plugin) Invite(ctx context.Context, in InviteRequest) error
Invite records an invitation for an address and mails it, as a signed-in member allowed to create invitations.
func (*Plugin) List ¶
func (p *Plugin) List(ctx context.Context) ([]OrgWithRole, error)
List returns the signed-in user's organizations.
func (*Plugin) ListAll ¶
func (p *Plugin) ListAll(ctx context.Context, in AllOrgsRequest) (AllOrgsResult, error)
ListAll pages through every organization of the app, newest first, with member counts. Limit defaults to DefaultMembersPage and is clamped to MaxMembersPage, as the members list is.
func (*Plugin) ListDomains ¶
ListDomains lists an organization's domains, as a member.
func (*Plugin) ListInvitations ¶
ListInvitations returns every invitation of an organization the signed-in user belongs to, newest first, whatever its state.
func (*Plugin) ListMembers ¶
func (p *Plugin) ListMembers(ctx context.Context, in OrgRequest) (MembersResult, error)
ListMembers is Members with paging, sort and filter.
func (*Plugin) ListMembershipRequests ¶
func (p *Plugin) ListMembershipRequests(ctx context.Context, orgID string) ([]MembershipRequest, error)
ListMembershipRequests lists an organization's pending requests, as a member with member:create.
func (*Plugin) ListRoles ¶
ListRoles lists an organization's dynamic roles, as a member with ac:read.
func (*Plugin) ListTeamMembers ¶
ListTeamMembers lists a team's members, as a member of its organization.
func (*Plugin) ListUserTeams ¶
ListUserTeams lists the teams the signed-in user is in, in every organization.
func (*Plugin) MarkDomainVerified ¶
MarkDomainVerified verifies a domain from the server, with no proof and no caller: for an operator who checked ownership another way. It is a Go API only, never a route. The domain must not be verified by another organization.
func (*Plugin) MembersFor ¶
func (p *Plugin) MembersFor(ctx context.Context, in OrgRequest) (MembersResult, error)
MembersFor pages through any organization's members with in's paging, sort and filter (in.OrgID names the organization; it is required here, since there is no session to supply an active one).
func (*Plugin) Operations ¶
Operations lists every operation; the team and role operations only when their option is on, so a route that cannot work is never mounted.
func (*Plugin) OptionErr ¶
OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).
func (*Plugin) PermissionsOf ¶
func (p *Plugin) PermissionsOf(ctx context.Context, orgID string, role Role) (access.Permissions, error)
PermissionsOf returns the permissions role grants in orgID: the union of every part of a (possibly comma-joined) role, from the static roles (DefaultRoles merged with WithRoles) and, with DynamicRoles, orgID's dynamic roles. A part that names no known role grants nothing. Actions are deduplicated and sorted, so a token claim built from it is deterministic, and the map is a fresh copy the caller may change. It reads roles only; what a custom Authorizer would decide is not reflected here.
func (*Plugin) RateLimits ¶
RateLimits bounds invitation mail (none without invitations, whose operation the rule would name) and verify-domain's DNS lookups.
func (*Plugin) RejectMembershipRequest ¶
RejectMembershipRequest declines a request, as a member with member:create; the organization is not suggested to that user again.
func (*Plugin) RemoveDomain ¶
RemoveDomain removes a domain and its pending membership requests, as a member with organization:update. Invitations it created stay, as any invitation does, until canceled or expired.
func (*Plugin) RemoveMember ¶
func (p *Plugin) RemoveMember(ctx context.Context, in MemberRequest) error
RemoveMember removes a member, or lets the caller leave; see doRemoveMember for who may remove whom.
func (*Plugin) RemoveMemberFor ¶
RemoveMemberFor removes a member from any organization, between the member hooks; the last holder of the creator role cannot be removed (ErrLastOwner), so no organization is left without one.
func (*Plugin) RemoveTeam ¶
RemoveTeam deletes a team and its memberships, as a member with team:delete.
func (*Plugin) RemoveTeamMember ¶
func (p *Plugin) RemoveTeamMember(ctx context.Context, in TeamMemberRequest) error
RemoveTeamMember removes someone from a team: themself, or anyone as a member with team:update.
func (*Plugin) RequirePermission ¶
func (p *Plugin) RequirePermission(orgID func(r *http.Request) string, req access.Request) func(http.Handler) http.Handler
RequirePermission is middleware that refuses a request unless the signed-in user's roles in the organization orgID names grant req. It must be inside authhttp.Middleware. A non-member gets ErrNotMember and a member without the permission ErrForbidden, as RequireRole answers.
Example ¶
ExamplePlugin_RequirePermission guards an app route by permission rather than by role. ds:def id=exampleplugin-requirepermission-ph8srha8 owner=@khanakia stability=stable
package main
import (
"fmt"
"net/http"
"github.com/ubgo/auth/plugin/access"
"github.com/ubgo/auth/plugin/organization"
)
func main() {
orgs := organization.New()
orgInPath := func(r *http.Request) string { return r.PathValue("org") }
createProject := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusCreated) })
mux := http.NewServeMux()
// Inside authhttp.Middleware in a real app.
mux.Handle("POST /orgs/{org}/projects", orgs.RequirePermission(orgInPath, access.Request{"project": access.All("create")})(createProject))
fmt.Println("mounted")
}
Output: mounted
func (*Plugin) RequireRole ¶
func (p *Plugin) RequireRole(orgID func(r *http.Request) string, role Role) func(http.Handler) http.Handler
RequireRole is middleware that refuses a request unless the signed-in user holds at least role in the organization orgID names — read from the path, a header or a subdomain, as the app routes. It must be inside authhttp.Middleware. A non-member gets ErrNotMember, a lower role ErrForbidden, so a stranger learns nothing about the organization.
func (*Plugin) Requires ¶
Requires names the email plugin when invitations are addressed by email, which they are by default; InviteBy without email, or WithoutInvitations, requires nothing.
func (*Plugin) RoleAuthorizer ¶
func (p *Plugin) RoleAuthorizer() Authorizer
RoleAuthorizer is the default Authorizer: allowed when one of the request's roles, static or dynamic in the request's organization, grants the whole request. A role that no longer exists grants nothing.
func (*Plugin) SessionEnded ¶
SessionEnded drops the ended session's selection.
func (*Plugin) SessionReplaced ¶
SessionReplaced moves a pending second-factor session's selection to the full session, so an organization chosen before the second factor is still active after it.
func (*Plugin) SetActive ¶
func (p *Plugin) SetActive(ctx context.Context, in SetActiveRequest) (ActiveResult, error)
SetActive makes an organization the signed-in session's active one, or clears the selection.
func (*Plugin) SetActiveTeam ¶
SetActiveTeam makes a team the session's active one (and its organization the active organization), or clears it with an empty id.
func (*Plugin) SetRole ¶
func (p *Plugin) SetRole(ctx context.Context, in SetRoleRequest) error
SetRole changes a member's role, as a signed-in member allowed to update members.
func (*Plugin) SetRoleFor ¶
SetRoleFor gives a member of any organization role. The caller acts with the creator role, so it may grant or take it, but the last holder still cannot be demoted (ErrLastOwner) and the role hooks run.
func (*Plugin) Sweep ¶
Sweep deletes invitations that expired unaccepted before cutoff, and the active selections of sessions whose hard stop is before cutoff. Accepted invitations stay: they record who invited whom.
func (*Plugin) Tables ¶
Tables declares every table this plugin can bring into a statement. The team and role tables are declared only when their option is on, so an app without teams has no auth_org_team to rename or migrate.
func (*Plugin) TypedMigrations ¶
TypedMigrations creates the tables for engine, or fails for an engine the core does not support. 0001 is the shipped schema; 0002 adds the organization's logo and metadata and the invitation's status and team; 0003 the active selection; 0004 and 0005 the team and role tables when their option is on; 0006 the invitation's identifier kind and value (InviteBy); 0007 the domain tables when Domains is on.
func (*Plugin) UpdateDomain ¶
UpdateDomain changes a domain's enrollment mode or role, as a member with organization:update.
func (*Plugin) UpdateFor ¶
UpdateFor changes any organization. ServerOnly fields are allowed, since the caller is trusted code.
func (*Plugin) UpdateRole ¶
UpdateRole renames a dynamic role or replaces its permissions, as a member with ac:update.
func (*Plugin) UpdateTeam ¶
UpdateTeam renames a team, as a member with team:update.
func (*Plugin) UserSessionsEnded ¶
UserSessionsEnded drops the selections of every session of userID but exceptID's.
func (*Plugin) UserTables ¶
UserTables declares the rows that belong to the user in user_id, so DeleteUser removes them (no foreign key does, ADR-0002): memberships, team memberships and active selections. Invitations keep their invited_by: they belong to the organization, not to the inviter.
func (*Plugin) Validate ¶
Validate asserts the plugin can function. Invitations to a kind no plugin registers would match nobody, and discovering that at start-up beats discovering it when the first invite goes nowhere. Requires only checks plugin names, so every kind is checked here too: a plugin that is merely named "email" does not make invitations deliverable.
type RequestIDRequest ¶
type RequestIDRequest struct {
RequestID string `json:"requestId"`
}
RequestIDRequest names a membership request, as the input of approve-membership-request and reject-membership-request.
type RequestStatus ¶
type RequestStatus string
RequestStatus is the state of a membership request made from a suggestion.
const ( RequestPending RequestStatus = "pending" RequestApproved RequestStatus = "approved" RequestRejected RequestStatus = "rejected" )
Membership request states.
type Role ¶
type Role string
Role is a membership role. A named type rather than a bare string so a role cannot be passed where an org id is expected — both are strings and they appear adjacent in almost every membership signature.
A member may hold several roles; they are stored comma-joined in one column ("admin,billing"), as better-auth stores them, and Parts splits them. JoinRoles builds one.
const ( //ds:def id=roleowner-szv8n5ye owner=@khanakia stability=stable RoleOwner Role = "owner" //ds:def id=roleadmin-cghbz3fj owner=@khanakia stability=stable RoleAdmin Role = "admin" //ds:def id=rolemember-k755wabe owner=@khanakia stability=stable RoleMember Role = "member" )
Built-in membership roles, granted DefaultRoles' permissions. An app adds its own with WithRoles (fixed at start-up) or DynamicRoles (created per organization at run time); every role check goes through the permission statements (HasPermission), never through the role's name.
func JoinRoles ¶
JoinRoles is the stored form of several roles, duplicates dropped, in the order given.
func (Role) Parts ¶
Parts splits a stored role into the roles it holds, trimmed, empty ones dropped.
func (Role) Rank ¶
Rank returns the privilege level of the built-in roles, higher being more privileged; for several roles, the highest. Custom roles rank 0: their power is their permissions, which HasPermission checks.
Derived from position in RoleValues rather than stored as a separate number, so adding a role in the right place is the only edit required and the two cannot disagree.
func (*Role) UnmarshalJSON ¶
UnmarshalJSON accepts a role as a string ("admin" or "admin,billing") or, as better-auth sends several, an array of strings.
type RoleRequest ¶
type RoleRequest struct {
OrgID string `json:"orgId,omitempty"`
RoleID string `json:"roleId,omitempty"`
RoleName Role `json:"roleName,omitempty"`
}
RoleRequest names a dynamic role by id, or by name when RoleID is empty, as the input of delete-role and get-role.
func (RoleRequest) AuditOrgID ¶
func (r RoleRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for delete-role and get-role.
type RoleResult ¶
type RoleResult struct {
Role Role `json:"role"`
}
RoleResult is the output of role: the caller's role in the organization.
type RoleUpdate ¶
type RoleUpdate struct {
RoleName *Role `json:"roleName,omitempty"`
Permission access.Permissions `json:"permission,omitempty"`
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
RoleUpdate is what update-role changes; nil stays.
func (*RoleUpdate) UnmarshalJSON ¶
func (r *RoleUpdate) UnmarshalJSON(data []byte) error
UnmarshalJSON keeps top-level field keys inside update-role's data.
type RolesResult ¶
type RolesResult struct {
Roles []OrgRole `json:"roles"`
}
RolesResult is the output of list-roles.
type SetActiveRequest ¶
type SetActiveRequest struct {
OrgID string `json:"orgId,omitempty"`
OrgSlug string `json:"orgSlug,omitempty"`
}
SetActiveRequest is the input of set-active: the organization to make active, by id or by slug. Both empty clears the selection, as better-auth's organizationId: null does.
func (SetActiveRequest) AuditOrgID ¶
func (r SetActiveRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for set-active.
type SetRoleRequest ¶
type SetRoleRequest struct {
OrgID string `json:"orgId,omitempty"`
UserID string `json:"userId"`
Role Role `json:"role"`
}
SetRoleRequest is the input of set-role. Role may hold several roles.
func (SetRoleRequest) AuditOrgID ¶
func (r SetRoleRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for set-role.
func (SetRoleRequest) TargetUserID ¶
func (r SetRoleRequest) TargetUserID() string
TargetUserID is MemberRequest's, for set-role.
type SlugRequest ¶
type SlugRequest struct {
Slug string `json:"slug"`
}
SlugRequest is the input of check-slug.
type SlugResult ¶
SlugResult says whether a slug may be used, and the normalized form that create would store. A slug that fails SlugPattern is not available.
type SuggestionsResult ¶
type SuggestionsResult struct {
Suggestions []DomainSuggestion `json:"suggestions"`
}
SuggestionsResult is the output of domain-suggestions.
type TXTResolver ¶
TXTResolver looks up TXT records; *net.Resolver is one.
type Team ¶
type Team struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
MemberCount int64 `json:"memberCount"`
// Fields are the extra columns WithFields declares.
Fields auth.Fields `json:"fields,omitempty"`
}
Team is a sub-group of an organization. MemberCount is counted when the team is read, not stored, so it cannot drift from the rows.
type TeamMember ¶
type TeamMember struct {
TeamID string `json:"teamId"`
User auth.UserView `json:"user"`
CreatedAt time.Time `json:"createdAt"`
}
TeamMember is one member of a team, with the user fields safe to show.
type TeamMemberChange ¶
TeamMemberChange is a team membership a team member hook sees.
type TeamMemberRequest ¶
TeamMemberRequest names a member of a team, as the input of add-team-member and remove-team-member.
func (TeamMemberRequest) TargetUserID ¶
func (r TeamMemberRequest) TargetUserID() string
TargetUserID is MemberRequest's, for add-team-member and remove-team-member.
type TeamMembersResult ¶
type TeamMembersResult struct {
Members []TeamMember `json:"members"`
}
TeamMembersResult is the output of list-team-members.
type TeamRequest ¶
type TeamRequest struct {
TeamID string `json:"teamId"`
}
TeamRequest names a team, as the input of remove-team, set-active-team and list-team-members. An empty TeamID clears the active team in set-active-team, and means the active team in list-team-members.
type TeamUpdate ¶
type TeamUpdate struct {
Name *string `json:"name,omitempty"`
Fields auth.Fields `json:"fields,omitempty"`
// contains filtered or unexported fields
}
TeamUpdate is what update-team changes; nil stays.
func (*TeamUpdate) UnmarshalJSON ¶
func (r *TeamUpdate) UnmarshalJSON(data []byte) error
UnmarshalJSON keeps top-level field keys inside update-team's data.
type TeamsConfig ¶
type TeamsConfig struct {
// MaxTeams caps the teams of one organization; zero is no cap.
MaxTeams int
// MaxMembersPerTeam caps the members of one team; zero is no cap.
MaxMembersPerTeam int
// AllowRemovingAllTeams lets an organization's last team be removed;
// by default it cannot be, so a team-scoped app always has one.
AllowRemovingAllTeams bool
// NoDefaultTeam stops create from making a team, with the creator in
// it, named after the organization (better-auth's defaultTeam.enabled
// false).
NoDefaultTeam bool
// DefaultTeamName names the default team (customCreateDefaultTeam);
// the organization's name when nil.
DefaultTeamName func(ctx context.Context, org Org, creator auth.User) (string, error)
}
TeamsConfig configures teams (Teams), better-auth's teams option.
type TeamsResult ¶
type TeamsResult struct {
Teams []Team `json:"teams"`
}
TeamsResult is the output of list-teams and list-user-teams.
type UpdateDomainRequest ¶
type UpdateDomainRequest struct {
DomainID string `json:"domainId"`
Enrollment *EnrollmentMode `json:"enrollmentMode,omitempty"`
Role *Role `json:"role,omitempty"`
}
UpdateDomainRequest is the input of update-domain; nil fields stay.
type UpdateRequest ¶
UpdateRequest is the input of update: the organization (the active one when OrgID is empty) and the changes.
func (UpdateRequest) AuditOrgID ¶
func (r UpdateRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for update.
type UpdateRoleRequest ¶
type UpdateRoleRequest struct {
OrgID string `json:"orgId,omitempty"`
RoleID string `json:"roleId,omitempty"`
RoleName Role `json:"roleName,omitempty"`
Data RoleUpdate `json:"data"`
}
UpdateRoleRequest is the input of update-role.
func (UpdateRoleRequest) AuditOrgID ¶
func (r UpdateRoleRequest) AuditOrgID() string
AuditOrgID is OrgRequest's, for update-role.
type UpdateTeamRequest ¶
type UpdateTeamRequest struct {
TeamID string `json:"teamId"`
Data TeamUpdate `json:"data"`
}
UpdateTeamRequest is the input of update-team.
type UserMembership ¶
type UserMembership struct {
// Role is the user's role in the organization the join was parameterised
// with. Empty when they are not a member, because the join is LEFT and the
// binding coalesces the null away.
Role Role
// JoinedAt is nil when they are not a member.
//
// A pointer rather than a bare time.Time because this column arrives via a
// LEFT join and database/sql cannot scan NULL into a time.Time. Coalescing
// it would mean inventing a fallback timestamp, which is worse than nil: a
// zero time reads as a real value from 1 January year 1 and will eventually
// be formatted into someone's interface.
JoinedAt *time.Time
}
UserMembership is this plugin's contribution to a composite user.
It demonstrates the other half of struct extension: unlike phone and username, which borrow the core's shared identity table under a private alias, this plugin contributes columns from a table it owns outright. Nothing in the Composite mechanism cares which of the two it is — a plugin supplies a join and bindings, and where the columns physically live is its own business.
Nested rather than flattened into the user struct, following the same rule as every other plugin here: the columns live in auth_org_member, a table of this plugin's own, so they read as u.Membership.Role rather than u.OrgRole.
func (*UserMembership) IsMember ¶
func (m *UserMembership) IsMember() bool
IsMember reports whether the user belongs to the organization the composite query was built for. Prefer it over comparing Role to "" at the call site, so the meaning of the empty role lives in one place.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package orgjwt puts the session's active organization into the tokens plugin/jwt mints: its id, slug, the caller's role in it and the permissions that role grants, Clerk's "o" organization claim.
|
Package orgjwt puts the session's active organization into the tokens plugin/jwt mints: its id, slug, the caller's role in it and the permissions that role grants, Clerk's "o" organization claim. |
|
Package orgtest is the organization counterpart of authtest's user factory (better-auth's ctx.test createOrganization / saveOrganization / addMember): build an organization in memory, then save it through the plugin's real Go API so every rule and hook runs.
|
Package orgtest is the organization counterpart of authtest's user factory (better-auth's ctx.test createOrganization / saveOrganization / addMember): build an organization in memory, then save it through the plugin's real Go API so every rule and hook runs. |