Restrict platform-list edits to admins

Anyone authenticated can still read the shared platform list, but only an
admin may add, rename or delete from it.

- users.is_admin boolean, defaulting to false. The very first account ever
  registered becomes an admin automatically (CreateUser's INSERT uses
  NOT EXISTS (SELECT 1 FROM users) for is_admin), so there's always at
  least one without manual setup; promoting anyone beyond that is a manual
  UPDATE for now (documented in the migration's comment on the column).
- GetSessionAndUser, GetUserByEmail and GetUserByID now also select
  is_admin, and it flows through to model.User (and so to
  /api/auth/{register,login,me}'s response) and to the auth middleware's
  authedUser.
- New requireAdminMiddleware + addAdminAuth, layered on top of the existing
  auth middleware. Wired onto create/update/delete-platform; list/get stay
  on plain addAuth, since reading the list only requires being logged in.

Verified end to end: the first registered user comes back is_admin:true,
the second false; the non-admin gets 403 on create/update/delete-platform
but 200 on list; the admin can do all of it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
SansanandClaude Sonnet 5 committed 2026-10-03 11:39:21 +02:00
1 parent 7fbd403d30
commit 384c4a7217
12 files changed
+110 -35

No files matched your search

+7
View File
@@ -14,6 +14,13 @@ CREATE TABLE users (
password_hash text NOT NULL,
name text NOT NULL,
bio text,
-- Only admins may add/rename/delete from the shared platform list (see
-- the platform table below); everyone can read it. The first account
-- ever registered becomes an admin automatically (see CreateUser in
-- db/query.sql) so there's always at least one without manual setup;
-- promoting anyone beyond that is a manual
-- `UPDATE users SET is_admin = true WHERE email = ...` for now.
is_admin boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
+9 -6
View File
@@ -1,18 +1,21 @@
-- name: CreateUser :one
-- The first account ever registered becomes an admin automatically; see
-- the is_admin column's comment in db/migrations for why and the race this
-- accepts. Everyone after that registers as a non-admin.
INSERT INTO users (
email, password_hash, name, bio
email, password_hash, name, bio, is_admin
) VALUES (
$1, $2, $3, $4
$1, $2, $3, $4, NOT EXISTS (SELECT 1 FROM users)
)
RETURNING id, email, name, bio, created_at;
RETURNING id, email, name, bio, is_admin, created_at;
-- name: GetUserByEmail :one
SELECT id, email, password_hash, name, bio, created_at
SELECT id, email, password_hash, name, bio, is_admin, created_at
FROM users
WHERE email = $1;
-- name: GetUserByID :one
SELECT id, email, name, bio, created_at
SELECT id, email, name, bio, is_admin, created_at
FROM users
WHERE id = $1;
@@ -24,7 +27,7 @@ INSERT INTO sessions (
);
-- name: GetSessionAndUser :one
SELECT users.id, users.email, users.name, users.bio, sessions.expires_at
SELECT users.id, users.email, users.name, users.bio, users.is_admin, sessions.expires_at
FROM sessions
JOIN users ON users.id = sessions.user_id
WHERE sessions.token_hash = $1 AND sessions.expires_at > now();