Documentation
¶
Overview ¶
Package invite is invitation codes for users, better-invite's plugin: a signed-in user creates an invite, either public (a link anyone holding it may use, up to a number of uses) or private (mailed to one address, used once), with an expiry and optionally a role; every use is recorded with who used it and when; and Required makes sign-up refuse anyone without a valid invite.
An invite is a token, stored as its hash. It reaches the sign-up in one of two ways, so every sign-up method works without knowing this plugin: the InviteHeader header on the sign-up request, or the invite cookie that activate sets for a signed-out visitor, which the provider callback of an OAuth sign-up carries too. A data hook on user creation spends it inside the sign-up's transaction, so an invite that runs out refuses the sign-up rather than leaving a user nobody invited.
The plugin stores a role but never grants one: OnAccepted runs in the same transaction with the role, for the app to apply (admin.SetRole, an organization membership, a plan). A role in an invite is refused unless the app passed CanCreate, so no user can mint an invite that makes its holder an admin.
Index ¶
- Constants
- Variables
- func DefaultText(inviter, link string) string
- type Accepted
- type Action
- type ActivateResult
- type Created
- type IDRequest
- type Invite
- type ListRequest
- type ListResult
- type Option
- func CanCreate(fn func(ctx context.Context, inviter auth.User, in Request) error) Option
- func HideInviter() Option
- func Links(fn func(t auth.Token) string) Option
- func OnAccepted(fn func(ctx context.Context, a Accepted) error) Option
- func PublicOnly() Option
- func Required() Option
- func WithCookieTTL(d time.Duration) Option
- func WithTTL(d time.Duration) Option
- type Plugin
- func (p *Plugin) Activate(ctx context.Context, tok auth.Token) (ActivateResult, error)
- func (p *Plugin) Cancel(ctx context.Context, id string) error
- func (p *Plugin) Create(ctx context.Context, in Request) (Created, error)
- func (p *Plugin) DataHooks() auth.DataHooks
- func (p *Plugin) Get(ctx context.Context, tok auth.Token) (View, error)
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) List(ctx context.Context, in ListRequest) (ListResult, error)
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) Reject(ctx context.Context, tok auth.Token) error
- func (p *Plugin) Tables() []sqlb.Table
- func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)
- func (p *Plugin) UserTables() []auth.UserTable
- func (p *Plugin) Validate(c *auth.Config) error
- type Request
- type Status
- type TokenRequest
- type View
Constants ¶
const ( EndpointCreate auth.EndpointID = "invite.create" EndpointGet auth.EndpointID = "invite.get" EndpointList auth.EndpointID = "invite.list" EndpointCancel auth.EndpointID = "invite.cancel" EndpointReject auth.EndpointID = "invite.reject" EndpointActivate auth.EndpointID = "invite.activate" )
Operation ids, stable wire names.
const ( // DefaultTTL is how long an invite works: one hour. DefaultTTL = time.Hour // DefaultCookieTTL is how long an activated invite waits in the cookie // for its visitor to sign up or sign in: ten minutes. DefaultCookieTTL = 10 * time.Minute // DefaultPage is the app page the link opens; it calls get to show the // invite and activate to use it. DefaultPage = "/invite" // MaxTTL bounds a requested expiry: an invite is a standing credential. MaxTTL = 30 * 24 * time.Hour // MaxListLimit bounds one page of list. MaxListLimit = 100 )
Defaults, better-invite's.
const ( TableNameInvite = "auth_invite" TableAliasInvite = "inv" TableNameUse = "auth_invite_use" TableAliasUse = "inu" ColID = "id" ColTokenHash = "token_hash" ColCreatedBy = "created_by" ColEmail = "email" ColRole = "role" ColMaxUses = "max_uses" ColUses = "uses" ColStatus = "status" ColExpiresAt = "expires_at" ColCreatedAt = "created_at" ColUseInvite = "invite_id" ColUseUser = "user_id" ColUseAt = "used_at" )
The tables: invites, and one row per use. No foreign keys (ADR-0002).
const CookieName = "invite"
CookieName is the plugin cookie activate sets for a signed-out visitor, read when the account is created or the visitor signs in.
const InviteHeader = "Invite-Token"
InviteHeader carries an invite token on a sign-up request. ds:def id=inviteheader-b6pq8rb7 owner=@khanakia stability=stable
const PluginName = "invite"
PluginName is the plugin's permanent identity and route prefix.
const PurposeInvite auth.Purpose = "invite.user"
PurposeInvite is the message a private invite sends.
Variables ¶
var ( TableInvite = sqlb.Table{Name: TableNameInvite, Alias: TableAliasInvite} TableUse = sqlb.Table{Name: TableNameUse, Alias: TableAliasUse} InviteID = sqlb.Col[string](TableInvite, ColID) InviteTokenHash = sqlb.Col[[]byte](TableInvite, ColTokenHash) InviteCreatedBy = sqlb.Col[string](TableInvite, ColCreatedBy) InviteEmail = sqlb.Col[*string](TableInvite, ColEmail) InviteRole = sqlb.Col[string](TableInvite, ColRole) InviteMaxUses = sqlb.Col[int64](TableInvite, ColMaxUses) InviteUses = sqlb.Col[int64](TableInvite, ColUses) InviteStatus = sqlb.Col[Status](TableInvite, ColStatus) InviteExpiresAt = sqlb.Col[time.Time](TableInvite, ColExpiresAt) InviteCreatedAt = sqlb.Col[time.Time](TableInvite, ColCreatedAt) UseInviteID = sqlb.Col[string](TableUse, ColUseInvite) UseUserID = sqlb.Col[string](TableUse, ColUseUser) UseAt = sqlb.Col[time.Time](TableUse, ColUseAt) )
The tables and their typed columns, for an app that reads them.
var ( ErrInvalid = auth.NewError("invite.invalid", http.StatusBadRequest, "This invitation is not valid or has expired.") ErrRequired = auth.NewError("invite.required", http.StatusForbidden, "An invitation is required to sign up.") ErrForbidden = auth.NewError("invite.forbidden", http.StatusForbidden, "You cannot create this invitation.") ErrNoBase = errors.New("invite: links need auth.WithBaseURL or invite.Links") ErrBadTTL = errors.New("invite: WithTTL and WithCookieTTL need a positive duration, WithTTL at most MaxTTL") ErrNoEmail = errors.New("invite: private invites mail an address, so the email plugin must be wired (or pass invite.PublicOnly)") )
Errors. ErrInvalid covers an unknown, expired, spent, canceled and rejected invite alike.
Functions ¶
func DefaultText ¶
DefaultText is the English body of a private invite's mail when the app's mailer has no template for PurposeInvite.
Types ¶
type Accepted ¶
type Accepted struct {
InviteID string
UserID string
CreatedBy string
Role string
// NewAccount is true when the use created the account.
NewAccount bool
}
Accepted is what OnAccepted receives when a user uses an invite.
type ActivateResult ¶
type ActivateResult struct {
Action Action `json:"action"`
}
ActivateResult is the answer of activate.
type Created ¶
type Created struct {
ID string `json:"id"`
Token auth.WireToken `json:"token,omitempty"`
URL string `json:"url,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
}
Created is the answer of create. Token and URL are given for a public invite only; a private one goes by mail.
type Invite ¶
type Invite struct {
ID string `json:"id"`
Email string `json:"email,omitempty"`
Role string `json:"role,omitempty"`
MaxUses int64 `json:"maxUses"`
Uses int64 `json:"uses"`
Status Status `json:"status"`
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
}
Invite is one invite in list: its use count, never its token.
type ListRequest ¶
ListRequest pages the caller's invites, newest first.
type ListResult ¶
ListResult is a page of list.
type Option ¶
type Option func(*Plugin)
Option configures the plugin.
func CanCreate ¶
CanCreate decides who may create which invite: return an error (such as ErrForbidden) to refuse. Without it every signed-in user may create an invite with no role, and any role is refused.
func HideInviter ¶
func HideInviter() Option
HideInviter leaves the inviter's name out of get, better-invite's shareInviterName false.
func Links ¶
Links builds the URL of an invite from its token, for an app whose page is not DefaultPage.
func OnAccepted ¶
OnAccepted runs in the transaction that records a use, to apply the role or anything else the invite stands for; an error undoes the use (and the sign-up it came with).
func PublicOnly ¶
func PublicOnly() Option
PublicOnly turns private invites off: a create that names an Email is refused with ErrForbidden, and the email plugin is no longer needed. Without it a private invite is mailed, so auth.New refuses the plugin with ErrNoEmail unless a plugin that delivers email (plugin/email) is wired, rather than the first private invite failing.
func Required ¶
func Required() Option
Required refuses a sign-up by a client (a public operation) that brings no valid invite. Users an admin or the app creates from Go are not refused.
func WithCookieTTL ¶
WithCookieTTL replaces DefaultCookieTTL.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the invite plugin. Build it with New.
func (*Plugin) Activate ¶
Activate uses the invite of tok for the signed-in caller, or keeps it in the cookie for a signed-out one.
func (*Plugin) DataHooks ¶
DataHooks spend the invite a request brings: on user creation, inside the sign-up's transaction, where a bad invite refuses the sign-up; and on session creation for an existing user who activated an invite while signed out, where a bad one is ignored so it never blocks a sign-in. ds:def id=plugin-datahooks-8kkfp2xr owner=@khanakia stability=stable
func (*Plugin) List ¶
func (p *Plugin) List(ctx context.Context, in ListRequest) (ListResult, error)
List returns the signed-in user's invites.
func (*Plugin) Operations ¶
Operations lists the six operations.
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) RateLimits ¶
RateLimits bounds lookups of a token and invites created per IP.
func (*Plugin) TypedMigrations ¶
TypedMigrations creates the two tables. A use's key is invite and user, so one user uses one invite once.
func (*Plugin) UserTables ¶
UserTables tells DeleteUser which rows are a user's: the invites they created, which stop working with them, and their uses.
type Request ¶
type Request struct {
// Email makes the invite private: mailed to this address and usable
// once; only the account with this address may reject it. The token is
// the proof, as better-invite's: whoever holds the mailed link may use
// it, which is how an invite to a work address can create an account
// under another. Empty makes a public invite whose token the creator
// shares.
Email string `json:"email,omitempty"`
// Role is stored and handed to OnAccepted; refused unless CanCreate is
// set.
Role string `json:"role,omitempty"`
// MaxUses bounds a public invite's uses; 0 means unlimited. A private
// invite is used once.
MaxUses int64 `json:"maxUses,omitempty"`
// ExpiresIn is seconds until the invite stops working; 0 means the
// plugin's TTL. At most MaxTTL.
ExpiresIn int64 `json:"expiresIn,omitempty"`
}
Request is the input of create.
type TokenRequest ¶
TokenRequest carries an invite token.
type View ¶
type View struct {
Status Status `json:"status"`
ExpiresAt time.Time `json:"expiresAt"`
Role string `json:"role,omitempty"`
InviterName string `json:"inviterName,omitempty"`
// Private is true for an invite mailed to one address.
Private bool `json:"private"`
}
View is what get shows the holder of a token.