rest_api_server (0.3.2)

Published 2026-09-15 21:39:33 +00:00 by faicel

Installation

[registries.forgejo]
index = "sparse+" # Sparse index
# index = "" # Git

[net]
git-fetch-with-cli = true
cargo add rest_api_server@0.3.2 --registry forgejo

About this package

Home IoT Backend (rest_api_server)

Important

The canonical repository for this project lives on Forgejo: https://code.bhk-itsolutions.com/homeiot/rest_api_server.git. This GitHub repository is only a mirror and is not the primary git remote.

This crate is the HTTP API backend for the Home IoT project. It exposes REST endpoints for:

  • User management (registration, update, deletion, authentication),
  • Sensor management (listing, pagination, filtering),
  • Authentication using JWT tokens stored in HTTP-only cookies,
  • OpenAPI documentation and Swagger UI,
  • TypeScript client generation scripts based on the OpenAPI spec.

The backend relies on a separate crate, db_handler, for all database access and data models.


1. Architecture overview

  • Crates

    • rest_api_server (this crate): HTTP API, routing, auth, OpenAPI.
    • db_handler: primary/history databases, repositories, models, shared error types.
  • Main modules

    • src/lib.rs
      • run_server(...): builds the Axum router, mounts routes, configures CORS, compression, Swagger UI.
      • generate_openapi(): combines OpenAPI fragments from user, sensor, and auth modules.
    • src/main.rs
      • Binary entry point (rest_api_server),
      • Loads configuration from CONFIG_FILE,
      • Initializes database connections via db_handler::db::connection::Connection,
      • Starts the HTTP server on 0.0.0.0:2606.
    • src/auth/*
      • JWT token generation & verification (handlers::token),
      • Login / logout / refresh handlers, cookie management,
      • Swagger/OpenAPI security scheme declaration (cookie-based auth).
    • src/user/*, src/sensor/*
      • REST handlers over db_handler services,
      • Pagination, validation, error handling.
    • src/tools/*
      • error.rs: centralized application error type (AppError),
      • state.rs: shared AppState (database connection + optional restart channel),
      • config.rs, responses.rs: configuration and response helper types.
    • src/bin/generate-openapi.rs
      • Standalone binary to export the OpenAPI spec into a JSON file.

2. Requirements

  • Rust toolchain: Rust stable with Cargo.

  • Databases

    • Primary database: SQLite (handled by db_handler with sqlx).
    • Time-series / history: InfluxDB 2.x (also configured in db_handler).
  • Configuration file

    • A JSON configuration file is required; its path is provided via the CONFIG_FILE environment variable (see below).
  • Registries

    • The db_handler crate is fetched from a custom Cargo registry named forgejo, configured in .cargo/config.toml:

      [registries.forgejo]
      index = "sparse+https://code.bhk-itsolutions.com/api/packages/homeiot/cargo/"
      

      You must ensure that this registry is reachable from your environment (it is public for this project).


3. Configuration

3.1 Application configuration file (CONFIG_FILE)

The backend expects the CONFIG_FILE environment variable to point to a JSON file. A typical pattern:

export CONFIG_FILE=/path/to/config.json

The JSON file is read twice:

  1. By this backend: the HTTP server parses it via serde into tools::config::Config. This struct has exactly one field, static_folder (required — the server fails to start if it is missing or the file is invalid JSON). This is the folder that serves the frontend SPA (a fresh clone does not ship its content; the deployment setup provides it).
  2. By db_handler when opening the databases: from the same file it reads the database fields primary_db_url (SQLite, e.g. :memory: for tests; migrations are applied automatically by db_handler), history_db_url, history_db_org and history_db_token (InfluxDB 2.x server URL, organization, auth token).

config.example.json and .env.example are tracked and provide a working placeholder configuration and environment template.

3.2 Environment variables

  • CONFIG_FILE — path to the JSON configuration file described above (required at startup).

  • SECRET_KEY — shared JWT secret (HS256 signing and verification). Required at startup: the server refuses to boot unless it is exactly 64 hexadecimal characters, i.e. 32 random bytes (256 bits) — the output format of:

    openssl rand -hex 32
    

    An arbitrary string of 32+ characters is not sufficient and does not provide 256-bit entropy. The key is validated once at startup and cached for the process lifetime; placeholders and malformed values abort the startup with an actionable error.

  • ENVIRONMENT — set to init only for a fresh install to enable the bootstrap mode described in section 4.0; anything else means production.

  • COOKIE_SECURE — optional override (true/false/1/0/yes/no) for the auth-cookie Secure flag. Unset, Secure is honored from X-Forwarded-Proto: https only in explicitly configured trusted-proxy mode (TRUST_PROXY_HEADERS=true); the default direct mode ignores the forgeable header (plain-LAN HTTP).

  • TLS_TERMINATED / TRUST_PROXY_HEADERS — startup-only transport posture (both default to direct mode): TLS_TERMINATED=true declares a TLS-terminating reverse proxy in front of this listener (authorizes the HSTS response header); TRUST_PROXY_HEADERS=true declares that the proxy owns the X-Forwarded-* forwarding headers.

  • ALLOWED_ORIGINS — optional, comma-separated CORS allowlist. Unset, only localhost / loopback origins are allowed.

  • RUST_LOG — optional, forwarded to tracing's EnvFilter (defaults to info).


4. Running the server

4.0 Init mode (ENVIRONMENT=init)

When ENVIRONMENT=init, the server starts in bootstrap mode:

  • POST /users is available without JWT (to create the first admin),
  • the SPA under static_folder is still served at / (same as production),
  • after the first user is created, the process restarts into production mode.

Set this only on a fresh install:

export ENVIRONMENT=init

If GET / returns 404 while in init mode, the running binary is outdated (pre–static-serving fix) or static_folder does not contain index.html.

If a browser refresh on a client route such as /users/ returns 404, rebuild and redeploy the backend: unknown GET paths must fall back to index.html so Vue Router can handle history-mode routes after a full page reload.

Then:

  1. Ensure db_handler is available and the databases are reachable:
    • Primary SQLite DB (and migrations will be applied by db_handler),
    • InfluxDB 2.x if you use history features.
  2. Prepare your configuration file and export CONFIG_FILE and SECRET_KEY (a fresh 256-bit key):
export CONFIG_FILE=/path/to/config.json
export SECRET_KEY="$(openssl rand -hex 32)"
  1. Run the server:
cargo run --bin rest_api_server

The server will bind to 0.0.0.0:2606 and log basic information via tracing.


5. OpenAPI & Swagger UI

5.1 Generating OpenAPI JSON

Use the dedicated binary, which builds the OpenAPI document from the code and writes it to the path you pass:

cargo run --locked --bin generate-openapi -- /tmp/openapi.json

5.2 Swagger UI in the running server

When the server is running, Swagger UI is served at:

  • GET /docs – Swagger UI frontend,
  • GET /api-doc/openapi.json – OpenAPI JSON served by the backend.

Authentication is configured via the CookieAuth security scheme using HTTP-only cookies (auth_token).


6. TypeScript client generation

The scripts/ directory contains two utilities to generate a TypeScript client from the OpenAPI spec:

  • scripts/generate-ts-client.sh — generates the OpenAPI JSON from code (via the generate-openapi binary) and then the TypeScript client in one step ([OUTPUT_DIR] argument).
  • scripts/generate-ts-client-from-file.sh — generates a TypeScript client from a local OpenAPI JSON file ([FILE] [OUTPUT_DIR] arguments).

Typical flow:

# Option 1: one step, from code
./scripts/generate-ts-client.sh ../frontTs/src/api

# Option 2: from a locally generated OpenAPI file
cargo run --locked --bin generate-openapi -- /tmp/openapi.json
./scripts/generate-ts-client-from-file.sh /tmp/openapi.json ../frontTs/src/api

7. Scripts overview

What a fresh clone actually contains under scripts/:

  • scripts/tests.sh

    • Exports CONFIG_FILE=config_test.json (a tracked test configuration),
    • Exports a deterministic 64-hex dummy SECRET_KEY satisfying the startup contract (test-only: it is public in this repository — never use it in production),
    • Runs cargo test -- --test-threads=1 --nocapture (see the tests section for why the single test thread is a hard contract).
  • scripts/generate-ts-client.sh

    • Generates the OpenAPI JSON from code and then the TypeScript client, in one step.
  • scripts/generate-ts-client-from-file.sh

    • Consumes a local OpenAPI JSON file and generates a TypeScript client.

The OpenAPI document itself is not checked in: regenerate it on demand with the inline command cargo run --locked --bin generate-openapi -- <output.json> (or fetch it from a running server at /api-doc/openapi.json).

Note

: the release/build/deployment helper scripts used on the target machine live outside this repository and are intentionally not tracked.

Security note: do not commit real secrets (keys, passwords, tokens) into these scripts. Use dummy values or rely on CI/CD environment variables.


8. Tests

8.1 Rust tests

Current suite: 53 tests — 48 unit tests plus 5 integration tests.

Layout follows the project convention: each module keeps its source file and its tests live in a tests/ directory next to it (e.g. src/user/handlers/get/mod.rs + src/user/handlers/get/tests/mod.rs), and the integration tests live in the tests/ target (tests/general/*.rs, tests/users/*.rs).

To run all tests from the backend directory:

./scripts/tests.sh

This script:

  • Exports CONFIG_FILE=config_test.json (tracked test configuration),
  • Exports a deterministic 64-hex dummy SECRET_KEY for JWT tests (test-only, never in production),
  • Runs cargo test -- --test-threads=1 --nocapture.

The --test-threads=1 single-thread contract is deliberate, not a workaround: the test suites share in-memory SQLite databases (:memory: migrations) and mutate process-wide environment variables (deployment mode, cookie policies), so running them in parallel would cause schema is locked SQLite errors and environment races between tests.

Note

: as a single Cargo integration target, the tests/ tree already uses one tests/mod.rs entry point and mod.rs files per submodule internally — this is the standard multi-module test-target structure, not a deviation from the project convention.

8.2 Integration tests behaviour

Integration tests:

  • Start an Axum HTTP server on a random local port,
  • Use reqwest clients (with or without cookie jar) to:
    • Register a user,
    • Log in and verify HTTP-only cookies,
    • Call authenticated routes (e.g. updating and reading users),
    • Verify error codes (401, 404, 400, etc.).

9. Error handling

  • All API-level errors use the AppError type (src/tools/error.rs), which:
    • Logs errors via tracing,
    • Maps them to appropriate HTTP status codes,
    • Returns generic or user-friendly messages to clients.
  • Database-layer errors are encapsulated in db_handler (e.g. CommonError), and then converted into AppError as needed.

10. Security model: flat-trust household administration

The authorization model is a deliberate, documented decision: every authenticated account is a household administrator. Any logged-in user may list, create, read, update and delete all users, including accounts they did not create. This is coherent both with the backend routes (every user-management route is protected by authentication only — there is no ownership check) and with the SPA, whose flows list all users, open arbitrary profiles, create users and delete them.

Exact trust boundary:

  • Requirement to manage users: any valid authenticated session (the HTTP-only auth_token cookie). There are no roles, no ownership isolation and no privilege levels anywhere in the API.
  • Consumer impact: a management UI or client built against this API can rely on any authenticated account being able to manage every user; conversely, per-user isolation cannot be assumed from this backend.
  • Residual risk: compromising any single account (weak password, stolen cookie, untrusted household member) grants full management of all household accounts, including creating new accounts and changing other users' passwords.

This model is only suitable for a trusted, single-household deployment (the LAN appliance use case). It is unsuitable for untrusted or multi-tenant users: one compromised account means one compromised household. Roles (e.g. administrator/member) are required before any untrusted or multi-tenant deployment; that is the explicit condition to re-evaluate this policy.

Related notes:

  • Every user-management route rejects unauthenticated requests with 401; the flat-trust behaviour itself (an authenticated account managing another user's account) is pinned by the authorization integration tests.
  • The model assumes a trusted LAN: network and DNS trust are part of this flat-trust deployment model, and Host-based same-origin checks do not defeat DNS rebinding by themselves.
  • Plaintext-LAN transport risk (accepted, managed): in the default direct mode the listener serves plain HTTP, so credentials, both JWT cookies and all API data transit the LAN unencrypted — any local observer can hijack a session. The mitigation path is a TLS-terminating reverse proxy: terminate HTTPS in front of the listener, then start the server with TLS_TERMINATED=true (enables HSTS) and TRUST_PROXY_HEADERS=true (honored forwarding headers, Secure cookies). Forwarded headers are never trusted in direct mode.
  • Local secret rotation: SECRET_KEY is validated once at startup (exactly 64 hex characters = 32 random bytes, generated with openssl rand -hex 32) and kept for the process lifetime. Rotating it means generating a fresh value and restarting the server, which invalidates every previously issued access and refresh token (users simply log in again). Keep real keys out of scripts and repositories (see section 11).

11. Notes on publishing

Before publishing this crate in a public Git repository, you should:

  1. Ensure there are no real secrets:
    • No real SECRET_KEY in scripts/tests.sh or any config file,
    • No .env, config.json, or other sensitive files tracked by Git.
  2. Decide how to distribute db_handler:
    • Either include it in the same repository,
    • Or publish it separately and reference it via git URL or crates.io (if you ever choose to).
  3. Keep placeholder configuration and environment files up to date:
    • config.example.json and .env.example are tracked — make sure they stay secret-free,
    • A README.DOCKER.md if you ever add Docker-based workflows.

Dependencies

ID Version
axum ^0.7
axum-extra ^0.9
chrono ^0.4.38
cookie ^0.18
db_handler =0.4.3
jsonwebtoken ^10.3.0
rand ^0.8
serde ^1.0.214
serde_json ^1.0.140
sqlx ^0.8.6
thiserror ^2.0.18
tokio ^1.41.0
tower ^0.5.3
tower-http ^0.6.8
tracing ^0.1.44
tracing-subscriber ^0.3.22
utoipa ^5.2.0
utoipa-swagger-ui ^8.0.3
validator ^0.20.0
reqwest ^0.13.2
Details
Cargo
2026-09-15 21:39:33 +00:00
0
109 KiB
Assets (1)
Versions (3) View all
0.3.2 2026-09-15
0.3.1 2026-09-15
0.2.1 2026-09-06