Skip to content

Flask Application

dps-code-api/app.py is the Flask application. It is primarily the composition and routing layer: it wires the environment, session, CORS, and route registration, and each route handler is a thin adapter — parse the request, apply gates, delegate to a service or shared operation, map the result to a response. Domain logic lives in the service modules under dps-code-api/.

Module layout at a glance

text
app.py                    routes, request handling, error mapping
  ├── environment.py      per-request environment config + sessions
  ├── authz.py            authentication/authorization gates
  ├── db_access.py        low-level data access + serialization
  ├── request_limits.py   body-size cap, input bounds
  ├── reports_service.py  report actions/statuses (shared operations)
  ├── punishment_service.py / discipline_queue.py / action_queue.py
  ├── event_service.py    canonical event stream interface
  ├── training_engine.py / training_service.py / training_runtime.py
  ├── agents_service.py / admin_service.py / health_service.py
  ├── oauth_service.py / contact_threads.py / erlc_relay.py
  └── demo_seed.py        demo-only dataset + reset

app.py re-exports the extracted helpers so existing callers and tests keep working; new code should import from the owning module directly.

Application setup

  • Flask(__name__) with SECRET_KEY (fallback), SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SECURE (default true), SESSION_COOKIE_SAMESITE=Lax, and MAX_CONTENT_LENGTH = 1 MiB.
  • app.session_interface = EnvironmentSessionInterface() — environment-bound signed cookies (see Environments).
  • Boot fails fast when the production environment is not configured.
  • An optional demo auto-reset scheduler starts as a daemon thread (DEMO_AUTO_RESET_ENABLED) that can only touch the demo database.
  • The ER:LC integration requires ERLC_RELAY_URLS + ERLC_RELAY_TOKEN (production) or ERLC_SERVER_KEY (local dev) at import time.

Request handling pipeline

text
request
 → before_request: environment resolution (fail closed 503) + body-size check
 → route handler:
     parse/validate input
     authentication (verify_api_key or active_agent_from_session)
     authorization (authorize_dashboard / require_admin / trainer gates)
     row-level checks (supervisor_report_denied, is_assigned_agent)
     service call → authoritative RPC or gated table write
     serialize response (identifier scrubbing)
 → after_request: CORS for allowlisted origins
 → error handlers: 404 / 405 / 500 → JSON error contract

Environment resolution

_require_request_environment runs before every request except the root liveness route. It resolves the environment from the trusted ingress hostname and returns {"success": false, "error": "API environment unavailable"} (503) when the host is unknown, the ingress assertion is invalid, or the environment is not configured. It also converts oversized bodies to the JSON error contract (413).

Authentication

  • Browser sessions: the signed session cookie carries the agent's discord_id. active_agent_from_session() (authz) fetches the liveagents row each request (cached per request), clears the session when the agent is missing or not active/onboarding, and returns the row.
  • Bot/integration: verify_api_key() compares the Authorization header to the active environment's API_KEY in constant time (cached per request).

Authorization

  • authorize_dashboard(permission) — API key (where a route allows the bot) or an active session with the required clearance; the reassign_docket permission additionally requires Head Investigator rank or above.
  • supervisor_report_denied(row) — row-level gate for supervisor-flagged reports.
  • require_admin(permission, director_only=False) — browser-session only: admin allowlist membership + clearance + (for Director-only capabilities) Director rank.
  • Training gates in training_runtime.py — browser session, trainer role, session-trainer ownership, trainee session membership.

Validation

Route handlers validate before touching the database: required fields, status enum membership, report-ID bounds (request_limits.validate_report_id), URL schemes (_validate_http_url), numeric bounds, and enum/whitelist membership. The transactional RPCs re-validate the critical values inside the database.

Service calls

Handlers delegate to shared operations, e.g.:

  • reports_service.perform_report_action(...) — the single application path for validate/invalidate/investigate/contact_reporter/claim; the mutation itself runs inside rpc_report_action.
  • reports_service.conclude_investigation(...) — the single conclude path.
  • punishment_service, discipline_queue, action_queue, admin_service — domain contracts and helpers.
  • event_service.record_event / record_report_event — canonical event writes from application code (the transactional RPCs write their own events in the same transaction).

Error handling

  • RPC errors are mapped tag → HTTP status (e.g. REPORT_NOT_FOUND → 404, STATUS_CONFLICT → 409, ACTION_ALREADY_COMPLETED → 409, DIRECTOR_REQUIRED → 403).
  • APIError (PostgREST) → 500 with the database message; unexpected exceptions → 500 "Unexpected server error".
  • Global handlers return the JSON contract for 404/405/500.

Response conventions

  • Success: {"success": true, ...} with a domain payload.
  • Failure: {"success": false, "error": "<message>"} with an appropriate status.
  • GET /actions/next (and queue pickups) return 204 with an empty body when there is nothing to do — an easy empty check for the bot.
  • Serializers are field whitelists (serialize_* in db_access.py); raw DB rows are never dumped to the client.
  • Raw user IDs are scrubbed from display text (_scrub_user_ids); identity lives in dedicated *_id fields.
  • Training mutations return slim {"success": true} payloads; the frontend re-fetches authoritative state (session creation is the exception — it returns the full session).

What must NOT live in app.py

Per the project's organization rules (see Code Organization):

  • Business rules belong in services (reports_service, punishment_service, training_*, …).
  • Gating logic belongs in authz.py / training_runtime.py.
  • Data access and serialization belong in db_access.py / training_service.py.
  • Configuration belongs in environment.py.
  • Authoritative multi-step mutations belong in the named RPCs.

If you find yourself adding a route that reaches for a table directly, first check whether an RPC already owns that mutation — and if not, whether it should (see RPC layer).