The invite and sign-up screens
The API calls behind the invite and sign-up screens.
On this page
Part of the HTTP API reference.
Three signed-out screens besides sign-in, each reached by its address and
answered before anything else is fetched. The signed-out router
(followSignedOut) stands down the moment a session exists, "so
it never competes with the application's own router."
| Address | Draw function |
|---|---|
#/invite/<token> |
drawInvite(token) |
#/signup |
drawSignUp |
#/signup/<token> |
drawSignupConfirm(token) |
GET /api/invites/{token} — is this link still good
Open. "Is this link still good, and whose is it? Reachable without a session."
The token is matched by its hash, never by itself.
Refusals
| Condition | Status | Message |
|---|---|---|
| unknown token | 404 | "That link is not one this repository gave out. Ask whoever invited you for another." |
| already used | 410 | "That link has already been used. Ask for another if you need to set a new password." |
| expired | 410 | "That link has expired. Ask whoever invited you for another." |
Response: {"username": "r.iyer", "display_name": "Rohan Iyer"}.
POST /api/invites/{token}/accept — set a password and sign in
Open.
Body: {"password": "three words you remember"} — at least 10 characters.
Refusals
| Condition | Status | Message |
|---|---|---|
| under 10 characters | 400 | "A password needs at least ten characters. Three words you would not forget beats something clever you would." |
| used, expired, unknown, or the account is inactive | 410 | "That link is no longer good. Ask whoever invited you for another." |
All four conditions share one sentence here, where the GET told three of them
apart — the row is locked FOR UPDATE and re-checked, so this is the
authoritative answer.
Side effects: the password hash is set, failed_count cleared, the invite
marked used, the superadmin flag cleared, every existing session for that
account revoked ("If somebody else was in their account, this is the moment
they are put out"), and a new session created.
Response: the _me shape, plus the session cookie.
GET /api/signup — is sign-up open
Open. "Reachable without a session; says nothing more."
Response: {"open": false, "why": "Sign-up is not open here. Ask whoever
runs this repository to invite you.", "hours": 24}. why is null when open,
and is always that one sentence when closed — never the administrator's
reason.
POST /api/signup — ask for an account
Open. Status 202 on success.
The rule this is built around — read it before touching anything here:
Sign-up must never tell anybody whether an address already has an account.
The sign-in endpoint already refuses to… A sign-up form is a far better oracle than a sign-in form — type an address, and "you already have an account" answers the question outright. So it does not say that. It says the same thing for a new address and a known one — "check your email" — and it is the EMAIL that differs: a new address gets a link to confirm, a known one gets "you already have an account here, sign in". The person at that inbox learns which. Nobody else does.
"The same thing" includes how long it takes. The password is hashed before anything is looked up, on every request, so a known address — which has no password to hash — does not come back fifty milliseconds sooner than a new one and give the game away by the clock.
Body
| Field | Type | Notes |
|---|---|---|
name |
string | whitespace collapsed; 1 to 120 characters |
email |
string | stripped, lower-cased; matched by EMAIL_RE; at most 254 characters |
password |
string | 10 to 1024 characters; may not equal the email address |
Refusals that may be answered plainly — "they are about the form, not about who else is here":
| Condition | Status | Message |
|---|---|---|
| no name | 400 | "Your name, please — it is how you are shown to everybody else here." |
| name over 120 | 400 | "That name is longer than 120 characters." |
| a bad email | 400 | "'<email>' is not an email address." |
| under 10 characters | 400 | "A password needs at least ten characters. …" |
| over 1024 | 400 | "That password is longer than this will take." |
| the password is the email | 400 | "Your password cannot be your email address." |
Refusals about the repository
| Condition | Status | Message |
|---|---|---|
| sign-up closed, for any of the five reasons | 403 | the SIGNUP_CLOSED sentence, never the real reason |
| the domain is not allowed | 403 | "Addresses at <domain> cannot sign up here. Ask whoever runs this repository to invite you." — names only the domain they typed, never the list |
| the mail could not be sent | 503 | "The confirmation email could not be sent just now. Try again in a few minutes." |
Throttling: one mail per address per SIGNUP_RESEND_MINUTES = 10, "whichever kind it would be" — and the throttled request is
answered exactly as a send would be, because "a different answer here
would be a way to ask 'did somebody just try this address', which is the
question the whole section refuses."
A second signup for one address replaces the first by expiring the earlier pending row, "so there is only ever one live way in per address, and it is the latest."
If the mail fails, the pending row is rolled back with it — "a link nobody was sent is a row that can only ever expire" — and the failure is recorded on a second connection so the log survives the rollback.
Response — identical in every success case, including the throttled one:
{"sent_to": "x@acme.com", "hours": 24}
SIGNUP_HOURS = 24.
Until that link is followed there is no app_user row, so POST
/api/session with the right credentials cannot sign the person in. It does not
answer the usual vague 401 either: it looks the pending sign-up up, checks the
password against it, and on a match answers 403 naming the address and
telling them to follow the link — or, past the 24 hours, telling them to sign up
again. See POST /api/session in Signing in.
GET /api/signup/{token} — is this confirmation link good
Open. The path must have exactly three slashes for the middleware to let it through.
Refusals (_signup_row)
| Condition | Status | Message |
|---|---|---|
| unknown | 404 | "That link is not one this repository sent. Sign up again to get a new one." |
| already used | 410 | "That link has already been used. Your account is ready — sign in." |
| expired | 410 | "That link has expired — they last 24 hours, and a newer sign-up puts an older link out. Sign up again." |
Response: {"email": "x@acme.com", "display_name": "X"}.
POST /api/signup/{token} — follow the link
Open. No body.
Docstring: "Follow the link: the account is made, given its role, and signed in."
The rules are checked AGAIN here, not only when the mail went out:
If an administrator closed sign-up, narrowed the domains or changed the role in the meantime, a link sent yesterday must not carry yesterday's rules into today.
Refusals
| Condition | Status | Message |
|---|---|---|
| unknown / used / expired | 404 / 410 | as the GET |
| sign-up closed, or the domain no longer allowed | 403 | "Sign-up has been closed since this link was sent. Ask whoever runs this repository to invite you." |
| an account for that address already exists | 409 | "There is already an account for this address. Sign in with it." — and the pending row is marked confirmed so the link cannot be retried |
The 409 names it plainly, and that is deliberate: "Holding the link proves the inbox, so saying the account exists is safe here in a way it was not on the form."
Side effects: an app_user row with username = email, note = "Made
through sign-up.", the configured role over the whole repository with the note
"Given on sign-up. Change it on People and Persona.", a signin logbook
line, and a session.
Response: the _me shape, plus the session cookie.