Admin service
Responsibility
- Admin authentication (OTP + JWT)
- Read-only access to game logs across all sessions
- Enriched audit timeline (actions + lifecycle events)
Location
Implemented as FastAPI module services/api/app/modules/admin/ mounted at /api/v1/admin/*.
Auth endpoints live under /api/v1/auth/admin/*.
Data model
game_events (new table)
Append-only events not captured as player actions.
| Column | Type | Description |
|---|---|---|
id |
UUID | PK |
game_instance_id |
UUID FK | Nullable for session-level events |
session_id |
UUID FK | Session reference |
event_type |
string | See event types below |
payload |
JSONB | Event-specific data |
player_id |
UUID | Nullable; actor for rejected actions |
created_at |
timestamptz | Server time |
Event types
| event_type | Trigger | payload |
|---|---|---|
game.started |
start_game |
plugin_id, initial_state, player_ids, host_id |
game.finished |
win or end_active_game |
winner_id, ended_by_host, final_state |
session.participant_joined |
join_session |
participant_id, display_name, channel, role |
session.closed |
close_session |
{} |
action.rejected |
failed submit_action |
action_type, payload, error_code, channel |
game_actions extension
| Column | Type | Description |
|---|---|---|
channel |
string | web or telegram — channel used for this action |
Existing columns unchanged: action_id, action_type, payload, player_id, result_state, created_at.
Admin API
All routes require get_current_admin dependency.
| Method | Path | Description |
|---|---|---|
| GET | /admin/games |
List games with filters and pagination |
| GET | /admin/games/{id} |
Game metadata + participants |
| GET | /admin/games/{id}/log |
Merged timeline |
| GET | /admin/sessions |
List sessions with game count |
| GET | /admin/sessions/{id}/games |
Games in session |
Timeline response shape
{
"game": {
"id": "uuid",
"plugin_id": "dice_board",
"session_id": "uuid",
"invite_code": "ABC123",
"status": "finished",
"started_at": "2026-06-14T12:00:00Z",
"finished_at": "2026-06-14T12:05:00Z",
"winner_id": "uuid"
},
"participants": [
{
"id": "uuid",
"display_name": "Alice",
"role": "host",
"control_channel": "web",
"guest_id": null,
"user_id": null
}
],
"timeline": [
{
"seq": 1,
"kind": "event",
"event_type": "game.started",
"at": "2026-06-14T12:00:00Z",
"payload": {}
},
{
"seq": 2,
"kind": "action",
"action_id": "uuid",
"action_type": "roll_dice",
"player_id": "uuid",
"player_name": "Alice",
"channel": "web",
"payload": {},
"result_state": {},
"at": "2026-06-14T12:00:15Z"
}
]
}
Timeline entries are sorted by created_at, then assigned seq.
Auth module
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/admin/otp/request |
Bot (telegram_id check) | Generate OTP |
| POST | /auth/admin/otp/verify |
Public | Verify OTP → admin JWT |
Redis keys:
admin:otp:{telegram_id}— OTP codeadmin:otp:attempts:{telegram_id}— verify attempt counteradmin:otp:rate:{telegram_id}— request rate limit
RBAC
async def get_current_admin(authorization: str = Header(None)) -> AdminContext:
# Parse JWT, require type == "admin"
Player JWT and guest tokens are rejected on admin routes.
Audit endpoint change
GET /games/{id}/audit — admin only (403 for unauthenticated/non-admin). Full detail available via /admin/games/{id}/log.
Retention
Aligned with NFR-027: game_actions and game_events retained 90 days (purge job future work).