Admin role, root/home URL split, activity permalinks
Three related changes.
1. **Admin role.** New `is_admin INTEGER NOT NULL DEFAULT 0` column on
users; added to MeResponse. Admin strictly implies moderator —
shared/roles.ts has a single isModerator()/isAdmin() pair so the
implication can't drift between callers. The duplicated isModerator()
helpers in server/activities.ts and server/feedback.ts now import
from there.
/api/admin endpoints (admin-only):
GET /admin/users — list users with their roles
PATCH /admin/users/:id/role — set is_moderator and/or is_admin
Last-admin guard: the role-update endpoint refuses to demote the only
remaining admin (409 cannot_demote_last_admin). Bootstrap is via
`sqlite3 ... UPDATE users SET is_admin=1` — documented in README.
Frontend Admin.svelte: table of users with toggles for moderator and
admin. Visible from the nav only when the current user is admin.
Toggling our own role refreshes session.user so the nav adapts
immediately.
2. **Root/home split.** The URL `/` always shows the public landing
(public + semi activities), even when the user is logged in. `/home`
is the authenticated dashboard. After login or signup the SPA pushes
`/home`; after logout it pushes `/`. popstate is wired so the
back/forward buttons work. Unknown paths fall through to the public
landing, not a 404.
3. **Activity permalinks at /a/:id.** New SPA route renders a single
activity via the existing GET /api/activities/:id endpoint (private
rows still require the owner's session to decrypt). A "Del" button
on each ActivityRow copies the absolute permalink to the clipboard.
Clipboard API has a prompt() fallback for environments where it's
blocked.
Server changes minimal: server/admin.ts is the new file; server/roles.ts
is the lifted helper; server/index.ts wires the admin routes; server/db.ts
gets one more ensureColumn() line.
26 tests still pass; typecheck clean; Vite build succeeds. Bundle grew
from 28.6 KB gzipped to 30.2 KB reflecting the Admin + permalink views.
This commit is contained in:
parent
f0b4d735b5
commit
bd82f71a01
16 changed files with 573 additions and 80 deletions
33
README.md
33
README.md
|
|
@ -143,18 +143,39 @@ Layout adapts to small screens via:
|
|||
- `min-height: 44px` on buttons (WCAG 2.5.5 enhanced touch target)
|
||||
- `font-size: 16px` on inputs below 480px so iOS doesn't auto-zoom
|
||||
|
||||
## Promoting a moderator
|
||||
## Roles: moderator and admin
|
||||
|
||||
Moderators can delete any `semi` or `public` activity (not `private` — those
|
||||
aren't visible to anyone else anyway). There's no admin UI; promotion is a
|
||||
one-liner against the SQLite file:
|
||||
There are three privilege levels:
|
||||
|
||||
| Role | What it grants |
|
||||
|-----------------|--------------------------------------------------------------------------------|
|
||||
| **Anonymous** | Browse public + semi activities, view opt-in `/<username>/list` pages |
|
||||
| **User** | + manage own activities, edit own profile, submit feedback |
|
||||
| **Moderator** | + delete any `semi`/`public` activity, read the feedback list |
|
||||
| **Admin** | + grant/revoke moderator and admin on other users (via `/api/admin/users`) |
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
# Bootstrap the first admin:
|
||||
sqlite3 data/vinterliste.db \
|
||||
"UPDATE users SET is_moderator = 1 WHERE email = 'olemd@example.org';"
|
||||
"UPDATE users SET is_admin = 1 WHERE email = 'you@example.org';"
|
||||
|
||||
# Promote a plain moderator (admins can also do this from the UI):
|
||||
sqlite3 data/vinterliste.db \
|
||||
"UPDATE users SET is_moderator = 1 WHERE email = 'them@example.org';"
|
||||
```
|
||||
|
||||
The user has to log out and back in for `MeResponse.is_moderator` to refresh.
|
||||
The user has to log out and back in for the in-memory `session.user` to
|
||||
refresh — server-side authz updates on the next request regardless.
|
||||
|
||||
A last-admin safety net is wired into the role-update endpoint: an admin
|
||||
trying to demote themselves while they're the only remaining admin gets a
|
||||
`409 cannot_demote_last_admin`. If you really want to strand the deployment
|
||||
with no admin, you have to use `sqlite3` directly.
|
||||
|
||||
## Manual verification
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue