69 lines
3.0 KiB
Markdown
69 lines
3.0 KiB
Markdown
---
|
|
type: database
|
|
title: User Table
|
|
description: The `user` table — accounts, roles, and statuses.
|
|
tags: [database, user, auth]
|
|
timestamp: 2026-07-21T17:37:18Z
|
|
---
|
|
|
|
# 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`).
|
|
|
|
# First-admin bootstrap
|
|
|
|
When the `user` table is empty of admins, the dedicated
|
|
`POST /api/v1/setup/create-admin` endpoint (see
|
|
[Setup Endpoint](/api/setup.md)) 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
|
|
|
|
- [Auth and Settings Tables](/database/auth-settings.md)
|
|
- [REST API Overview](/api/rest-overview.md)
|
|
- [Admin Endpoints](/api/admin.md)
|