Files
terdut-server/internal/db
Niklas Ye d827ceedff
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 14s
CI / test (pull_request) Successful in 2m30s
Add self-service sign-up and invite links
First half of #7. Until now the only way to get an account was for
somebody who already had one to create it, and the login page told people
to "ask an admin" -- workable for one operator, impossible for a team.

Two modes, chosen by an administrator in the settings table: invite_only,
which is the default, and open. A third domain-restricted mode was
considered and dropped, because with no email in this server there is
nothing to verify an address against and it would only check the domain
of a string somebody typed.

The default is the closed door. An install that gets a public hostname
before anybody has thought about sign-up should not be collecting
accounts from the internet, and the failure mode of a typo in the setting
is invite_only rather than open.

An invite is a link, not an email. Adding SMTP to send one message would
be a subsystem to run, secure and monitor; the person inviting sends the
link however they already talk to the person they are inviting. A link
carries the team and the role, because an account in no team sees an
empty queue and can be paged by nobody -- that is not a state to invite
somebody into. Links are single-use by default, expire after seven days,
and can be revoked before that: a link that works forever is a credential
nobody remembers issuing, sitting in a chat log.

The uses counter is incremented inside the sign-up transaction and
guarded by `uses < max_uses`, so two people redeeming the last use at
once cannot both get in.

GET /api/signup reports the mode and whether a link is usable, so the
form can say "this link has expired" before somebody picks a password
rather than after. It gives one answer for expired, revoked, used up and
never existed: telling a stranger which it was tells them something about
links they do not hold.

Sign-up signs you in. The alternative is a form that says "now go and log
in", which is the same credential typed twice. login and signup now share
startSession rather than each minting a cookie.

Rate-limited per address on its own limiter, not login's: a burst of
sign-ups must not lock somebody out of logging in.

The settings table grew a second shape for this. It held only durations;
signup_mode is a word from a fixed list, so the admin endpoint now
validates everything before writing anything -- a request that sets two
settings and gets one wrong changes neither.

Still to come in #7: the sign-up and invite-redemption pages, the
first-run checklist, and the in-app integration instructions. The schema
carries onboarding_dismissed_at for the checklist already.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
2026-09-21 09:06:48 +02:00
..