-- Two independent auth defects that share a migration because they share a table.

-- 1. RESERVED NAMES — free any guest squatting on a name the admin path used to depend on.
--
-- Migration 007 made display_name unique per event case-insensitively, and `join` had no
-- reserved-name guard. So any guest could join as "admin"/"Admin"/"ADMIN" before the operator's
-- first admin login; admin_login then looked its user up BY NAME, missed (wrong role), fell
-- through to creating "Admin", violated that unique index, and returned a 500 — permanently,
-- with no in-app recovery. Moderation, config and gallery release all gone, fixed only by SQL.
--
-- The real fix is in code (look the admin up by role, never by name — see auth/handlers.rs).
-- This clears the state an already-deployed database may be carrying.
--
-- RENAMED, NEVER DELETED: the guest keeps their uploads, their PIN and their session. Only
-- non-admin rows are touched — a real admin row named "Admin" is the expected state.
-- Two guards that are not optional, because this statement runs INSIDE the migration
-- transaction on boot and a failure here exits the process — `restart: unless-stopped` then
-- turns it into a crash loop with no in-app recovery. That is a strictly worse version of the
-- lockout this migration exists to clean up after.
--
--   * role = 'guest', not role <> 'admin'. The enum also has 'host' (001), and hosts are
--     promoted from guests at runtime — so <> 'admin' renamed a legitimately promoted staff
--     member whose name happens to be "Host".
--   * NOT EXISTS. The target name is derived, not unique: `idx_user_event_name_ci` (007) is a
--     UNIQUE index on (event_id, lower(display_name)), and nothing stopped a second guest from
--     having already joined as exactly "Admin (a1b2c3d4)" — the old code had no reserved-name
--     guard and the join response hands each guest their own id. Rare, but the cost of losing
--     that bet is the whole event.
--
-- A row that collides is simply left alone: `create_admin_user` already handles a name clash by
-- falling back to `Admin-<8hex>`, and `admin_login` no longer resolves by name at all, so this
-- cleanup is convenience rather than load-bearing.
UPDATE "user" u
   SET display_name = u.display_name || ' (' || left(u.id::text, 8) || ')'
 WHERE u.role = 'guest'
   AND lower(u.display_name) IN ('admin', 'administrator', 'host', 'eventsnap')
   AND NOT EXISTS (
        SELECT 1 FROM "user" x
         WHERE x.event_id = u.event_id
           AND lower(x.display_name) = lower(u.display_name || ' (' || left(u.id::text, 8) || ')')
   );

-- 2. PIN LOCKOUT DECAY.
--
-- failed_pin_attempts only ever cleared on a successful recovery or after a lockout expired, so
-- honest typos accumulated across days: a guest who fat-fingered their PIN twice last night
-- arrives today already two-thirds of the way to being locked out. With the threshold now
-- raised (see below) a decay window is what keeps that raise safe rather than merely lenient.
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS last_failed_pin_at TIMESTAMPTZ;

-- Rate-limit knobs introduced with this release.
--
-- recover_name_rate_per_15min (4, was a hardcoded 5): the per-(IP, name) ceiling. It MUST stay
-- below the account-lock threshold, which is the whole defect — at 5-per-IP against a 3-strike
-- lock, three requests from one IP locked any guest whose name is visible on the feed, every 15
-- minutes, forever. The lock threshold moves to 12 in code, so locking a victim now needs at
-- least three distinct sources while an honest guest never comes close.
--
-- pin_reset_ip_rate_per_min (30): /recover/request was the one unauthenticated endpoint with no
-- per-IP ceiling at all — /join got one in 017 and /recover in 019, and this third one was
-- simply missed. Its per-name key is attacker-chosen, so cycling names minted a fresh bucket
-- every time and the per-IP cost was unbounded.
--
-- upload_edit_rate_per_min (30): PATCH /upload/{id} had no rate limit of any kind.
INSERT INTO config (key, value) VALUES
    ('recover_name_rate_per_15min', '4'),
    ('pin_reset_ip_rate_per_min',   '30'),
    ('upload_edit_rate_per_min',    '30'),
    ('upload_edit_rate_enabled',    'true')
ON CONFLICT (key) DO NOTHING;
