Files
HIPCTF2/docs/database/users.md
T
2026-07-23 13:28:26 +00:00

3.6 KiB

type, title, description, tags, timestamp
type title description tags timestamp
database User Table The `user` table — accounts, roles, and statuses.
database
user
auth
2026-07-23T13:27:25Z

Schema

Column Type Description
id TEXT PK UUID v4 generated on creation.
username TEXT Unique login name.
password_hash TEXT Argon2id hash (memory/time/parallelism from env config).
role TEXT 'admin' or 'player'. Default 'player'.
status TEXT 'enabled' or 'disabled'. Default 'enabled'.
created_at TEXT ISO 8601 timestamp (strftime('%Y-%m-%dT%H:%M:%fZ','now')).

Constraints

  • Unique on username.
  • role is enforced application-side (see AdminService.updateUserRole).
  • status is checked by AuthService.login and AuthService.refresh — disabled users cannot log in or refresh.

Invariants

  • The application enforces the last-admin invariant: at least one enabled user with role = 'admin' must always exist. Attempts to demote or delete the last admin are rejected by UsersService.enforceLastAdminInvariant (used by AdminService.updateUserRole and AdminService.deleteUser).
  • A deployment-wide database safety net backs the invariant up at the SQLite level via two triggers created by the AddUserLastAdminTriggers1700000000800 migration: trg_user_last_admin_update (fires on UPDATE OF role when the old row was admin, the new role is not, and no other admin row exists) and trg_user_last_admin_delete (fires on DELETE of an admin row when no other admin row exists). Both abort the offending statement with RAISE(ABORT, 'LAST_ADMIN'). They are recreated by the restore rebuild after the candidate database is rebuilt — see Admin System Endpoints — POST /restore/commit.

First-admin bootstrap

When the user table is empty of admins, the dedicated POST /api/v1/setup/create-admin endpoint (see Setup Endpoint) creates the very first admin and auto-issues a session. Once any admin row exists the endpoint rejects further attempts with 409 SYSTEM_INITIALIZED, so the route is effectively hidden in production.

The legacy POST /api/v1/auth/register-first-admin route is preserved for compatibility but the canonical first-admin flow now lives in the SetupModule.

Concurrent bootstrap safety

Two near-simultaneous POST /api/v1/setup/create-admin requests cannot both succeed. SetupService.createAdmin serializes callers on an in-process promise chain (#bootstrapChain + private runBootstrap() in backend/src/modules/setup/setup.service.ts:111-130) so that the first request commits inside its DataSource.transaction before the second request's transaction executes. The loser's transaction observes adminCount > 0 and throws ApiError(SYSTEM_INITIALIZED, 409); the winner is the only request that creates a UserEntity(role='admin') row and mints a refresh token. The lock is per-process; multi-replica deployments rely on the unique index and transaction guard, not on this chain. The regression test is tests/backend/setup-create-admin.spec.ts ("only one of two concurrent first-admin requests succeeds").

See also