Self-registry toggle, invite links with attribution, first-user-admin
Three pieces of a single registration story.
1. **Self-registry toggle.** New generic `settings` key/value table.
Initial key: `self_registry_enabled` (default `1`). Admin-only PATCH
/api/settings flips it. GET /api/settings is public so the login
screen can hide the "Opprett konto" CTA when registration is closed.
2. **Invite links.** New `invites(token, inviter_user_id, created_at,
claimed_at, claimed_by_user_id)` table; tokens are 22-char base64url
(~128 bits of entropy). Endpoints:
POST /api/invites — create (any logged-in user)
GET /api/invites — list mine
DELETE /api/invites/:token — cancel an unclaimed invite
Claimed invites are kept in the DB (the audit trail of who-invited-
whom survives) — only unclaimed ones can be cancelled.
The signup endpoint accepts an optional `invite_token`. The signup
handler does the claim + user-insert in a single SQLite transaction
so we can't end up with a claimed invite pointing at a missing user.
A concurrent claim race is closed by `UPDATE … WHERE claimed_at IS
NULL` — only one transaction's UPDATE actually flips the column.
New `users.invited_by` column records the inviter id so accounts have
a traceable origin. Profile page shows the user's invites with
"Kopier lenke" / "Avbryt" buttons; the SPA serves /invite/<token>
into the Signup view with the token prefilled.
3. **First-user auto-admin.** The signup handler counts users *before*
the insert; if it's the first one, `is_admin` is set on the row.
This solves the bootstrap chicken-and-egg without an env var or
sqlite3 step. Documented in README.
When self-registry is **off**:
- The login screen hides "Opprett konto" and shows a "stengt" notice
- /api/auth/signup with no invite returns 403 signup_closed
- /api/auth/signup with a valid invite still works (and attributes)
- /api/auth/signup with an *invalid* invite returns 403 invalid_invite
When self-registry is **on**:
- Anyone can sign up (no invite required)
- An invite that comes along is still consumed for attribution
- An invalid invite is ignored — signup proceeds without attribution
26 tests still pass; typecheck clean; bundle 31.2 KB → 32.7 KB gzipped.
This commit is contained in:
parent
5c9455c3f3
commit
755a615f61
14 changed files with 567 additions and 37 deletions
24
README.md
24
README.md
|
|
@ -126,6 +126,22 @@ podman run --replace --name vinterliste \
|
|||
The container exposes `/api/health` for healthchecks and bakes the build date /
|
||||
git revision into both OCI labels and `/etc/build-info`.
|
||||
|
||||
## Registration: open, invite-only, or both
|
||||
|
||||
Admins can toggle self-registration from the Admin UI. Two modes:
|
||||
|
||||
- **Open** (default): anyone can hit `/` → "Logg inn" → "Opprett konto".
|
||||
- **Closed**: the "Opprett konto" button is hidden; new accounts can only
|
||||
be created through invite links.
|
||||
|
||||
Any logged-in user can generate invite links from their Profile page. Each
|
||||
link is single-use; the link URL is `/<origin>/invite/<token>` and the
|
||||
recipient is dropped straight into the signup form with the token attached.
|
||||
|
||||
Successful invite-signups record `users.invited_by = <inviter_id>` so the
|
||||
account has a traceable origin. Invites that have been claimed are kept in
|
||||
the DB (they can no longer be cancelled) so the audit trail survives.
|
||||
|
||||
## Installable (PWA) + mobile
|
||||
|
||||
The SPA ships with a web app manifest (`/manifest.webmanifest`), an SVG icon
|
||||
|
|
@ -156,11 +172,13 @@ There are three privilege levels:
|
|||
|
||||
Admin implies moderator — admins automatically pass any `is_moderator` check.
|
||||
|
||||
The **first admin** has to be promoted out of band (chicken-and-egg). After
|
||||
that, admins can grant moderator/admin to others through the Admin UI.
|
||||
The **first user** to sign up is auto-promoted to admin so a fresh
|
||||
deployment is never stranded without one. After that, admins can grant
|
||||
moderator/admin to others through the Admin UI. If for any reason that
|
||||
didn't happen (e.g., you imported a DB), you can still bootstrap manually:
|
||||
|
||||
```bash
|
||||
# Bootstrap the first admin:
|
||||
# Manually promote (e.g. for imported DBs):
|
||||
sqlite3 data/vinterliste.db \
|
||||
"UPDATE users SET is_admin = 1 WHERE email = 'you@example.org';"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue