Files
Completed/db/migrations/20261003000000_init.up.sql
SansanandClaude Sonnet 5 384c4a7217 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>
2026-10-03 11:39:21 +02:00

83 lines
3.2 KiB
PL/PgSQL

-- Requires pgcrypto for gen_random_uuid(); UUIDs are v7 when the app generates
-- them (see internal/model), but the DB default falls back to v4 for rows
-- inserted outside the app (e.g. manual SQL).
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS citext;
-- change_seq is a single global sequence shared by platform and game so a
-- client can ask "what changed since N?" across both tables in one pass.
CREATE SEQUENCE change_seq_seq;
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email citext NOT NULL UNIQUE,
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()
);
CREATE TABLE sessions (
token_hash bytea PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
last_seen_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
-- Platforms are shared: every user picks from and adds to the same list,
-- unlike games, which are private per user_id.
CREATE TABLE platform (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
change_seq bigint NOT NULL DEFAULT nextval('change_seq_seq'),
UNIQUE (name)
);
CREATE INDEX idx_platform_change_seq ON platform(change_seq);
CREATE TABLE game (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
platform_id uuid NOT NULL REFERENCES platform(id),
name text NOT NULL,
score int NOT NULL CHECK (score BETWEEN 0 AND 100),
release_date date NOT NULL,
finished date,
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
change_seq bigint NOT NULL DEFAULT nextval('change_seq_seq')
);
CREATE INDEX idx_game_user_change_seq ON game(user_id, change_seq);
-- Keep updated_at/change_seq current on every UPDATE so sync can detect
-- changes without clients having to set them manually.
CREATE OR REPLACE FUNCTION touch_change_seq() RETURNS trigger AS $$
BEGIN
NEW.updated_at := now();
NEW.change_seq := nextval('change_seq_seq');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER platform_touch_change_seq
BEFORE UPDATE ON platform
FOR EACH ROW EXECUTE FUNCTION touch_change_seq();
CREATE TRIGGER game_touch_change_seq
BEFORE UPDATE ON game
FOR EACH ROW EXECUTE FUNCTION touch_change_seq();