Documentation
¶
Overview ¶
Package signup decides whether this instance admits new accounts, and admits them.
One idea runs through all of it. **`LINKCTRL_SIGNUP_MODE` is the mode, and the operator is the only one who sets it** (D38). There is no stored toggle, no permission, and no endpoint: changing how an instance admits accounts is an `.env` edit and a restart. What this package computes on top of that variable is one derivation and no policy — `open` with no mailer is `invite`, because there would otherwise be no way to verify an address.
Three consequences are worth stating before the code.
**`open` requires a configured mailer** (D1). Open registration proves the address before an account exists at all: the form writes a pending registration and mails a link, and the user, organization and workspace are created when the link is followed. With no relay configured there is nothing to prove an address with, so the effective mode drops to `invite` — and the signup page refuses on GET rather than letting somebody fill a form in and discover it at submit time.
**`closed` admits no new account by any path** (D7), which is why this package rather than the environment answers internal/invite's question about whether redemption may create an account. A mode that closed the signup form but left invitations creating accounts would make the word mean two things.
**A self-registered account gets its own organization and workspace** (D6), which is the opposite of an invited one. That difference is the whole reason this milestone ships after invitations, and the form says it in words.
Index ¶
- Constants
- Variables
- type Config
- type Enqueuer
- type Mode
- type RegisterInput
- type Registered
- type Service
- func (s *Service) Configured() Mode
- func (s *Service) Effective() Mode
- func (s *Service) MailerConfigured() bool
- func (s *Service) PurgeLapsed(ctx context.Context) (int64, error)
- func (s *Service) Register(ctx context.Context, in RegisterInput) (*Registered, error)
- func (s *Service) Verify(ctx context.Context, token string) (*Verified, error)
- type Verified
Constants ¶
const ConsumedRetentionDays = 7
ConsumedRetentionDays is how long a spent registration row is kept before the sweep removes it. Short, because the account it produced is the durable evidence and the audit log holds the rest.
const MailKind = "verification"
MailKind names the mail template, which is also the outbox's `kind` column.
const MailKindExists = "account-exists"
MailKindExists is what a registration attempt on an address that already has an account sends instead. It exists so the *response* does not have to say so: the mail reaches the address, and only its owner reads it, where a status code reaches whoever typed the address into the form (F13).
const TokenBytes = 32
TokenBytes is the entropy in a verification token, matching an invitation's.
const VerificationTTL = 24 * time.Hour
VerificationTTL is how long an emailed verification link stays usable.
A constant rather than a variable, unlike INVITE_TTL. An invitation's window is an administrator's policy about somebody else's onboarding, and D29 made it tunable for that reason; this window is a person finishing something they started minutes ago, and one day is generous for that without being a credential anybody has to think about. Registering again supersedes the old link, so nobody is ever stuck waiting for this to lapse.
Variables ¶
var ( // ErrClosed is registration refused because the effective mode is not // `open`. Returned from both the form and the API, and from verification — // an operator who closes sign-ups stops the accounts that were half-way // through, because D7's bound is absolute rather than a moment. ErrClosed = errors.New("signup: this instance does not accept sign-ups") // ErrNotVerifiable is every failure to complete a verification: no such // token, expired, already spent. One error for all of them, because the // holder of a bad link learns nothing from which. ErrNotVerifiable = errors.New("signup: this link is no longer valid") // ErrEmailTaken is an address that already has an account. ErrEmailTaken = auth.ErrEmailTaken )
Errors this package returns that a caller distinguishes.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Mode is LINKCTRL_SIGNUP_MODE, and it is the whole of the policy (D38).
Mode Mode
// AppURL is the origin a verification link points at.
AppURL string
// Hasher hashes the password chosen at the form, at the cost parameters the
// operator configured. Required: a hasher this package invented for itself
// would use costs nobody chose.
Hasher *auth.Hasher
// Mail delivers the verification link. Nil is an instance with no relay, and
// it is what lowers an `open` mode to `invite` — there is no other way to
// prove an address, and `open` without one would be an open door with a
// verification step that never happens (D1).
Mail Enqueuer
}
Config is what a Service needs. Its own struct rather than config.Config, matching every other service in this tree.
type Enqueuer ¶
type Enqueuer interface {
Enqueue(ctx context.Context, to, kind string, data map[string]string) error
}
Enqueuer is internal/mail's writing half, as this package needs it.
Declared here rather than imported so "no mailer configured" is a nil interface rather than a flag every call site has to remember to check, and so a test satisfies it in four lines.
type Mode ¶
type Mode string
Mode is how open this instance is to new accounts.
Its own type rather than internal/config's, for the reason every service package here declares its own configuration: the package that does the work does not read the environment. The values are the same three words, so the wiring converts with a string conversion and no table of equivalences.
const ( // Closed admits no new account by any path, invitations included (D7). Closed Mode = "closed" // Invite admits an account only through a redeemed invitation, where an // administrator named the address first. Invite Mode = "invite" // Open additionally admits anybody through the signup form, once they have // proven the address. Open Mode = "open" )
func (Mode) AdmitsNewAccounts ¶
AdmitsNewAccounts reports whether an account may be created at all — the question internal/invite asks before letting a redemption create one.
type RegisterInput ¶
RegisterInput is somebody filling in the signup form.
type Registered ¶
type Registered struct {
// Email is the normalized address the link was sent to. Echoed back so the
// page can say which inbox to look in.
Email string
// ExpiresAt is when the link stops working.
ExpiresAt time.Time
}
Registered is what a caller tells the person afterwards.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service answers what the instance's signup mode is, and runs the two halves of an open registration.
func (*Service) Configured ¶
Configured is `LINKCTRL_SIGNUP_MODE` as the operator set it, before the mailer is taken into account. Logged at boot beside Effective, so "you configured `open` but there is no relay" is one line in the log rather than a mystery at the signup form.
func (*Service) Effective ¶
Effective is the mode that actually applies.
`LINKCTRL_SIGNUP_MODE`, lowered to `invite` when no mailer is configured. That is the one derivation this package performs, and it exists because open registration verifies an address by email (D1): with no relay, `open` would be an open door with a verification step that never runs.
No context and no error, because there is nothing to read. The mode is fixed for the life of the process, which is what makes "no session or API call can change it" a property of the shape rather than of a check.
func (*Service) MailerConfigured ¶
MailerConfigured reports whether this instance can prove an address.
func (*Service) PurgeLapsed ¶
PurgeLapsed removes registrations nobody completed and spent rows past the short retention window, reporting how many went. Called by the maintenance job, for the reason the outbox has a purge: a waiting room with no sweep is the one table that grows forever with nothing watching it.
func (*Service) Register ¶
func (s *Service) Register(ctx context.Context, in RegisterInput) (*Registered, error)
Register starts an open-mode registration.
It creates no account. Under D1 the address is proven first, so this writes a pending registration and queues the mail that carries the link; the user, the organization and the workspace are written by Verify, in one transaction, when somebody demonstrates they read mail at the address.
func (*Service) Verify ¶
Verify completes a registration, creating the account it was waiting on.
D6 in one function: a self-registered account gets its own organization and its own workspace, and owner membership in it — which is exactly what an invited account does not get. auth.ProvisionOrganization is called rather than reimplemented, so there is one statement of what provisioning tenancy means.
The effective mode is checked again here, and that is not belt-and-braces. A link lives for a day and an operator can close the instance inside that window — an `.env` edit and a restart — so a registration started while sign-ups were open must not still be able to land afterwards. D7's bound is a state the instance is in, not a moment a request passed through.