Skip to content

REST API

The backend's REST API is annotated with utoipa and an OpenAPI specification is generated from the route registration.

Generating the spec offline

The spec is generated locally, on demand. A running server serves neither interactive API documentation nor a raw spec endpoint, so a public self-hosted deployment does not publish a readable inventory of its API surface — and the frontend client is generated offline anyway (see below), so nothing needs a runtime spec.

Use the dump-openapi subcommand to produce one:

bash
cargo run -p lcxl-remote-desk-server -- dump-openapi --out openapi.json

The subcommand exports straight from the route registration; it touches no DB or Redis and starts no HTTP server, so it runs against any checkout.

Regenerating the Frontend Client

The frontend client (vite-project/src/services/) is generated from the OpenAPI spec with Kubb. After changing the backend API, regenerate it (offline dump — no running server required):

bash
cd vite-project
npm ci        # installs the exact Kubb version the lockfile pins
# Windows:
.\update_openapi.ps1
# Linux/macOS:
./update_openapi.sh

The scripts use that same dump-openapi subcommand to export the spec offline. The spec is passed to Kubb through a temporary file and deleted afterward; generated openapi.json files are not tracked in the repository.

Kubb is pinned to an exact version and invoked as npx --no-install, so the generator can neither drift across patch releases nor be silently downloaded when dependencies are missing — the committed client stays reproducible from the lockfile. Run npm ci first if the regeneration fails to find Kubb.

TIP

Generated files under vite-project/src/services/ are produced by Kubb — do not edit them by hand.

Regeneration is not optional

npm run build only runs tsc and vite; it never regenerates the client. A backend change that alters the spec therefore leaves a stale client that still compiles — and a changed numeric value produces no error at all, it just keeps being sent. CI regenerates on every push and fails if the result differs from what is committed.

Authentication contract

Browser/controller authentication uses one canonical JSON surface:

  • POST /api/auth/login
  • POST /api/auth/logout
  • GET /api/auth/me
  • PATCH /api/auth/credentials
  • POST /api/auth/tauri-login (standalone desktop host only)
  • GET /api/init/requirements (public, read-only; reports whether a bootstrap token is required)
  • POST /api/init (public before initialization, optionally bootstrap-token gated)

Every response uses RestResponse. Credential/business failures from login and credentials remain HTTP 200 with success=false; an absent or expired session on /api/auth/me is HTTP 401 with the same JSON envelope. Public fields use snake_case. OAuth authorization/callback continuation remains under /api/oauth/* and is not part of this route family.

Account login failures keep HTTP 200. ILLEGAL_CREDENTIALS does not distinguish the username from the password. At the configured threshold the response code is ACCOUNT_LOCKED and LoginOutcomeDto.retry_after_sec carries the remaining whole-second lock time; captcha_required is explicitly false in the standalone build. Bootstrap failures use PERMISSION_ERROR, while an exhausted bootstrap/probe quota uses TOO_MANY_ATTEMPTS.

Error Codes

DeskErrorCode (utils/src/error.rs) is declared through the desk_error_codes! macro, which emits both the constants and an ALL name/value table. That table is published in the spec as an int32 enum carrying x-enum-varnames, so the generated client exposes deskErrorCodeEnum with named members — frontends branch on those instead of mirroring the numbers by hand.

Nothing references the type in a request or response body (RestResponse.code is a bare integer on the wire), so it reaches the spec only through the explicit registration in server/src/openapi.rs. Adding a code means adding one line to the macro and regenerating.

The frontend maps codes to text through src/lib/desk-error-i18n.ts, where each area keeps a small table of the codes it can receive. The fallback for an unmapped code is the caller's choice: show the backend message, or show a localized generic line. A verify-error-codes check runs before every build and rejects a code written as a bare number, so the generated constants stay the only source.