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>
83 lines
3.2 KiB
PL/PgSQL
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();
|