Files
minstrel/internal/db/migrations/0022_user_management.up.sql
T
bvandeusenandClaude Opus 5.5 6028d78844
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m33s
release / go (push) Successful in 1m49s
release / integration (push) Successful in 5m42s
release / android (push) Successful in 5m59s
release / Build signed APK (releases and dev) (push) Successful in 6m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
docs: migration headers keep the before and after, without ids, quotes or one library's numbers (#6277)
Applies rule 32's migration exception: each header still says what the
schema was, what changes and what it prevents, but task, milestone,
decision and rule ids are replaced by the practice they stood for, the
operator quotes by the decision, single-install counts are dropped, and
pointers to a deleted package and an external design doc are gone.
golang-migrate keeps no checksum, so editing applied migrations'
comments is safe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:34:39 -04:00

49 lines
2.0 KiB
SQL

-- User management:
--
-- - users.display_name: optional human-readable name distinct from username.
-- - user_invites: admin-generated tokens; user redeems during signup.
-- - registration_settings (singleton): controls whether registration is
-- 'invite_only' (default after first admin) or 'open' (admin can
-- re-enable). The registration handler also has a special-case for the
-- "no users yet" state where it ignores invites and lets the first
-- register as admin.
-- - audit_log: append-only record of admin-driven user mutations.
-- No retention policy.
ALTER TABLE users ADD COLUMN IF NOT EXISTS display_name text;
CREATE TABLE user_invites (
token text PRIMARY KEY,
invited_by uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
note text,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
redeemed_at timestamptz,
redeemed_by uuid REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX user_invites_active_idx
ON user_invites (expires_at)
WHERE redeemed_at IS NULL;
CREATE TABLE registration_settings (
id boolean PRIMARY KEY DEFAULT true,
mode text NOT NULL DEFAULT 'invite_only',
CONSTRAINT registration_settings_singleton CHECK (id = true),
CONSTRAINT registration_settings_mode_check
CHECK (mode IN ('invite_only', 'open'))
);
INSERT INTO registration_settings (id) VALUES (true)
ON CONFLICT (id) DO NOTHING;
CREATE TABLE audit_log (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
actor_id uuid REFERENCES users(id) ON DELETE SET NULL,
target_id uuid REFERENCES users(id) ON DELETE SET NULL,
action text NOT NULL,
metadata jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX audit_log_created_at_idx ON audit_log (created_at DESC);
CREATE INDEX audit_log_actor_id_idx ON audit_log (actor_id);
CREATE INDEX audit_log_target_id_idx ON audit_log (target_id);