docs: reorder & testing
This commit is contained in:
@@ -31,13 +31,14 @@ CREATE INDEX device_sessions_user_idx ON device_sessions (user_id);
|
||||
CREATE INDEX device_sessions_status_idx ON device_sessions (status);
|
||||
|
||||
CREATE TABLE auth_challenges (
|
||||
challenge_id uuid PRIMARY KEY,
|
||||
email text NOT NULL,
|
||||
code_hash bytea NOT NULL,
|
||||
attempts integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
consumed_at timestamptz
|
||||
challenge_id uuid PRIMARY KEY,
|
||||
email text NOT NULL,
|
||||
code_hash bytea NOT NULL,
|
||||
attempts integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
consumed_at timestamptz,
|
||||
preferred_language text NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX auth_challenges_email_idx ON auth_challenges (email);
|
||||
@@ -48,6 +49,30 @@ CREATE TABLE blocked_emails (
|
||||
blocked_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- session_revocations is the durable audit trail of every device-session
|
||||
-- revocation. Each revoke writes one row carrying the actor kind, actor
|
||||
-- id, and free-form reason. The table is append-only; reading it is the
|
||||
-- only way to answer "who and why revoked this session". The
|
||||
-- device_session_id column is not a foreign key because device_sessions
|
||||
-- rows survive after revoke (status='revoked'), and dropping a session
|
||||
-- through a future cleanup must not implicitly drop its audit history.
|
||||
CREATE TABLE session_revocations (
|
||||
revocation_id uuid PRIMARY KEY,
|
||||
device_session_id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
actor_kind text NOT NULL,
|
||||
actor_user_id uuid,
|
||||
actor_username text,
|
||||
reason text NOT NULL DEFAULT '',
|
||||
revoked_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT session_revocations_actor_chk
|
||||
CHECK (actor_user_id IS NULL OR actor_username IS NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX session_revocations_user_idx ON session_revocations (user_id, revoked_at DESC);
|
||||
CREATE INDEX session_revocations_device_idx ON session_revocations (device_session_id, revoked_at DESC);
|
||||
CREATE INDEX session_revocations_actor_kind_idx ON session_revocations (actor_kind, revoked_at DESC);
|
||||
|
||||
-- =====================================================================
|
||||
-- User domain
|
||||
-- =====================================================================
|
||||
@@ -64,14 +89,17 @@ CREATE TABLE accounts (
|
||||
preferred_language text NOT NULL,
|
||||
time_zone text NOT NULL,
|
||||
declared_country text,
|
||||
permanent_block boolean NOT NULL DEFAULT false,
|
||||
deleted_actor_type text,
|
||||
deleted_actor_id text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
permanent_block boolean NOT NULL DEFAULT false,
|
||||
deleted_actor_type text,
|
||||
deleted_actor_user_id uuid,
|
||||
deleted_actor_username text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
CONSTRAINT accounts_email_unique UNIQUE (email),
|
||||
CONSTRAINT accounts_user_name_unique UNIQUE (user_name)
|
||||
CONSTRAINT accounts_user_name_unique UNIQUE (user_name),
|
||||
CONSTRAINT accounts_deleted_actor_chk
|
||||
CHECK (deleted_actor_user_id IS NULL OR deleted_actor_username IS NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX accounts_listing_idx
|
||||
@@ -88,19 +116,22 @@ CREATE INDEX accounts_declared_country_idx
|
||||
-- shape used by sanction_records/limit_records: the *_active rollup carries
|
||||
-- only the binding, the records table is the durable audit log.
|
||||
CREATE TABLE entitlement_records (
|
||||
record_id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES accounts (user_id),
|
||||
tier text NOT NULL,
|
||||
is_paid boolean NOT NULL,
|
||||
source text NOT NULL,
|
||||
actor_type text NOT NULL,
|
||||
actor_id text,
|
||||
reason_code text NOT NULL DEFAULT '',
|
||||
starts_at timestamptz NOT NULL DEFAULT now(),
|
||||
ends_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
record_id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES accounts (user_id),
|
||||
tier text NOT NULL,
|
||||
is_paid boolean NOT NULL,
|
||||
source text NOT NULL,
|
||||
actor_type text NOT NULL,
|
||||
actor_user_id uuid,
|
||||
actor_username text,
|
||||
reason_code text NOT NULL DEFAULT '',
|
||||
starts_at timestamptz NOT NULL DEFAULT now(),
|
||||
ends_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT entitlement_records_tier_chk
|
||||
CHECK (tier IN ('free', 'monthly', 'yearly', 'permanent'))
|
||||
CHECK (tier IN ('free', 'monthly', 'yearly', 'permanent')),
|
||||
CONSTRAINT entitlement_records_actor_chk
|
||||
CHECK (actor_user_id IS NULL OR actor_username IS NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX entitlement_records_user_idx
|
||||
@@ -117,32 +148,41 @@ CREATE TABLE entitlement_snapshots (
|
||||
is_paid boolean NOT NULL,
|
||||
source text NOT NULL,
|
||||
actor_type text NOT NULL,
|
||||
actor_id text,
|
||||
actor_user_id uuid,
|
||||
actor_username text,
|
||||
reason_code text NOT NULL DEFAULT '',
|
||||
starts_at timestamptz NOT NULL,
|
||||
ends_at timestamptz,
|
||||
max_registered_race_names integer NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT entitlement_snapshots_tier_chk
|
||||
CHECK (tier IN ('free', 'monthly', 'yearly', 'permanent'))
|
||||
CHECK (tier IN ('free', 'monthly', 'yearly', 'permanent')),
|
||||
CONSTRAINT entitlement_snapshots_actor_chk
|
||||
CHECK (actor_user_id IS NULL OR actor_username IS NULL)
|
||||
);
|
||||
|
||||
CREATE TABLE sanction_records (
|
||||
record_id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES accounts (user_id),
|
||||
sanction_code text NOT NULL,
|
||||
scope text NOT NULL,
|
||||
reason_code text NOT NULL,
|
||||
actor_type text NOT NULL,
|
||||
actor_id text,
|
||||
applied_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz,
|
||||
removed_at timestamptz,
|
||||
removed_by_type text,
|
||||
removed_by_id text,
|
||||
removed_reason_code text,
|
||||
record_id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES accounts (user_id),
|
||||
sanction_code text NOT NULL,
|
||||
scope text NOT NULL,
|
||||
reason_code text NOT NULL,
|
||||
actor_type text NOT NULL,
|
||||
actor_user_id uuid,
|
||||
actor_username text,
|
||||
applied_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz,
|
||||
removed_at timestamptz,
|
||||
removed_by_type text,
|
||||
removed_by_user_id uuid,
|
||||
removed_by_username text,
|
||||
removed_reason_code text,
|
||||
CONSTRAINT sanction_records_code_chk
|
||||
CHECK (sanction_code IN ('permanent_block'))
|
||||
CHECK (sanction_code IN ('permanent_block')),
|
||||
CONSTRAINT sanction_records_actor_chk
|
||||
CHECK (actor_user_id IS NULL OR actor_username IS NULL),
|
||||
CONSTRAINT sanction_records_removed_by_chk
|
||||
CHECK (removed_by_user_id IS NULL OR removed_by_username IS NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX sanction_records_user_idx
|
||||
@@ -161,19 +201,25 @@ CREATE TABLE sanction_active (
|
||||
CREATE INDEX sanction_active_code_idx ON sanction_active (sanction_code);
|
||||
|
||||
CREATE TABLE limit_records (
|
||||
record_id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES accounts (user_id),
|
||||
limit_code text NOT NULL,
|
||||
value integer NOT NULL,
|
||||
reason_code text NOT NULL,
|
||||
actor_type text NOT NULL,
|
||||
actor_id text,
|
||||
applied_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz,
|
||||
removed_at timestamptz,
|
||||
removed_by_type text,
|
||||
removed_by_id text,
|
||||
removed_reason_code text
|
||||
record_id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES accounts (user_id),
|
||||
limit_code text NOT NULL,
|
||||
value integer NOT NULL,
|
||||
reason_code text NOT NULL,
|
||||
actor_type text NOT NULL,
|
||||
actor_user_id uuid,
|
||||
actor_username text,
|
||||
applied_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz,
|
||||
removed_at timestamptz,
|
||||
removed_by_type text,
|
||||
removed_by_user_id uuid,
|
||||
removed_by_username text,
|
||||
removed_reason_code text,
|
||||
CONSTRAINT limit_records_actor_chk
|
||||
CHECK (actor_user_id IS NULL OR actor_username IS NULL),
|
||||
CONSTRAINT limit_records_removed_by_chk
|
||||
CHECK (removed_by_user_id IS NULL OR removed_by_username IS NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX limit_records_user_idx
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- +goose Up
|
||||
-- Persist the locale captured at send-email-code so it can be replayed at
|
||||
-- confirm-email-code when the auth flow needs `preferred_language` to seed
|
||||
-- a freshly-created `accounts` row. Existing rows default to '' and are
|
||||
-- treated by the auth service as "no captured locale", in which case the
|
||||
-- service falls back to the geoip-derived language and finally to "en".
|
||||
|
||||
ALTER TABLE backend.auth_challenges
|
||||
ADD COLUMN preferred_language text NOT NULL DEFAULT '';
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE backend.auth_challenges
|
||||
DROP COLUMN preferred_language;
|
||||
@@ -0,0 +1,26 @@
|
||||
# Backend migrations
|
||||
|
||||
Goose migrations embedded into the backend binary by `embed.go`. Applied
|
||||
at startup before any listener opens (see `internal/postgres`).
|
||||
|
||||
## Pre-production single-file rule
|
||||
|
||||
**While the platform is not yet in production, every schema change goes
|
||||
into the existing `00001_init.sql` file** rather than a new
|
||||
`00002_*`-prefixed file. The intent is to keep the schema in one
|
||||
canonical place so reviewers and developers do not have to reconstruct
|
||||
the latest shape from a chain of incremental migrations.
|
||||
|
||||
Operationally this means that pulling a branch with schema changes
|
||||
requires a fresh database — the only consumer today is local development
|
||||
and integration tests, both of which spin up disposable Postgres
|
||||
instances.
|
||||
|
||||
> **Remove this rule before the first production deployment.** From
|
||||
> that point on every schema change must be a new migration file with a
|
||||
> monotonically increasing prefix, and `00001_init.sql` becomes
|
||||
> immutable history.
|
||||
|
||||
If you need to make a change, edit `00001_init.sql` directly. Down
|
||||
migrations should still be kept in sync (they live at the bottom of the
|
||||
file — currently a single `DROP SCHEMA backend CASCADE`).
|
||||
Reference in New Issue
Block a user