db_handler (0.4.4)
Installation
[registries.forgejo]
index = "sparse+ " # Sparse index
# index = " " # Git
[net]
git-fetch-with-cli = truecargo add db_handler@0.4.4 --registry forgejoAbout this package
db_handler
Important
The canonical repository for this project lives on Forgejo: https://code.bhk-itsolutions.com/homeiot/db_handler.git. This GitHub repository is only a mirror and is not the primary git remote.
Rust library for the Home IoT backend: primary database (SQLite), time-series history (InfluxDB 2.x), and domain services (sensors, users, keys, refresh tokens, nonces).
Features
- Primary DB: SQLite with sqlx, migrations, connection pooling
- History DB: InfluxDB 2.x client (rustls), sensor history (e.g. contact sensors)
- Domain: Sensors (typed models, contact sensors), users (Argon2id), keys, refresh tokens, nonces
- Optional: OpenAPI docs via
utoipaDocfeature
Prerequisites
- Rust 1.75+ (2021 edition)
- SQLite
- InfluxDB 2.x (for history features; optional for tests via Docker)
- Dependency:
db_derive— not on crates.io; use path, Git, or the Forgejo package registry (see Building).
Building
db_derive is required.
Git dependency:
In Cargo.toml, replace the path with one of:
# Default branch (e.g. main)
db_derive = { git = "https://code.bhk-itsolutions.com/homeiot/db_derive.git" }
# Pin to a tag (recommended for reproducible builds)
db_derive = { git = "https://code.bhk-itsolutions.com/homeiot/db_derive.git", tag = "v0.1.0" }
# Or pin to a specific commit
db_derive = { git = "https://code.bhk-itsolutions.com/homeiot/db_derive.git", rev = "abc1234" }
With Git, tag and rev are exact (one tag = one version). For version ranges (e.g. accept all 0.2.x), use the Forgejo package registry below.
Option C — Forgejo package registry (version ranges):
If db_derive is published on your Forgejo instance, you can use Cargo’s package registry and semver ranges.
- One-time setup — in
~/.cargo/config.tomladd (replace URL and owner if needed):
[registries.forgejo]
index = "sparse+https://code.bhk-itsolutions.com/api/packages/homeiot/cargo/"
- In
Cargo.toml, use the registry and a version requirement:
db_derive = { version = "0.2", registry = "forgejo" }
This uses Cargo semver (e.g. "0.2" ⇒ ^0.2, so any 0.2.x). For publishing db_derive to Forgejo, see Forgejo Cargo registry.
Then:
cd db_handler
cargo build --features "sensors,users,utoipaDoc"
Configuration
The application reads configuration from a JSON file. Set the path via the CONFIG_FILE environment variable (default: config.json in the current directory).
Example config (copy from config.example.json and fill in):
{
"primary_db_url": "sqlite:data/home_iot.db",
"history_db_url": "http://localhost:8086",
"history_db_org": "my-org",
"history_db_token": "your-influxdb-token",
"bucket_name": "my-bucket"
}
primary_db_url: SQLite connection URL (e.g.sqlite:path/to/file.dbor:memory:for tests).history_db_url: InfluxDB 2.x URL (e.g.http://localhost:8086).history_db_org: InfluxDB organization name.history_db_token: InfluxDB API token.bucket_name(optional): InfluxDB bucket that history writes target. Defaults totest_bucket; set it to your production bucket per deployment.
Do not commit real credentials. Use config.example.json as a template and keep config.json or config.local.json local (they are in .gitignore).
At-rest protection of the SQLite database is the deployer's responsibility: the primary database file is plain SQLite (no encryption backend is bundled). Restrict file permissions (e.g. user-only access to the data directory) and/or rely on system-level encryption (LUKS, FileVault, BitLocker) as appropriate for your deployment.
The configuration file carries the InfluxDB token in plaintext, so restrict it to the service user as well (e.g. chmod 600 config.json, owner-only), exactly as prescribed for the database file.
Running tests
Run tests in Docker (recommended; includes InfluxDB). If Cargo.toml uses the Forgejo registry for db_derive, set a read-only token (e.g. in .env): CARGO_REGISTRIES_FORGEJO_TOKEN=your_token. See .env.example.
./scripts/test-docker.sh
See README.DOCKER.md for options (--keep, manual commands, etc.).
Troubleshooting (Docker tests): If the 4 set_history tests fail with InfluxDB error "no space left on device", Docker has run out of disk space. Free space and retry: docker system prune -a (removes unused images/containers), and/or increase the Docker Desktop virtual disk size in settings.
To run tests locally without Docker (in-memory SQLite, no InfluxDB):
./scripts/test.sh
This runs the default suite with --test-threads=1 (four set_history tests are
#[ignore]). To run InfluxDB integration tests locally:
cargo test -- --ignored set_history
Or use ./scripts/test-docker.sh for the full suite including InfluxDB.
Project layout
src/– Library codedb/– Primary DB connection, migrations; history DB connectionsensors/– Sensor models, repository, service; contact sensor and historyhistory/– InfluxDB-backed history (sensors, keys)users/,keys/,refresh_token/,nonces/– Domain servicescommon/– Config, helpers
migrations/– SQLite migrations (sqlx)scripts/– Test and Docker helper scriptsconfig.test.json– Test config (safe to commit; uses test token)config.example.json– Example config (no secrets)
Features (cargo)
sensors– Sensor and contact sensor supportusers– User domain (Argon2id login) andrefresh_tokendomain (REST refresh rotation + gRPC session API); see Authentication and session tokensutoipaDoc– OpenAPI documentation (addsutoipadependency)
Build with the features you need, for example:
cargo build --features "sensors,users,utoipaDoc"
Authentication and session tokens
Everything in this section requires the users feature, which gates the
users domain together with the refresh_token domain as one closure.
Passwords (breaking in 0.4.0). New and reset passwords are hashed with Argon2id (standard PHC strings, unique OS-CSPRNG salt per hash, default RustCrypto parameters) and must be 8–128 bytes; anything else is rejected with a typed error on creation/reset, and login attempts with out-of-range passwords fail uniformly before any hashing work. There is no legacy fallback: bcrypt hashes are intentionally unsupported in 0.4.0, every pre-existing account fails closed until its password is reset through an approved provisioning/reset path. Email stays the sole login identifier and lookups are byte-exact: emails are never trimmed, lowercased or case-folded. Login runs exactly one Argon2id verification (unknown emails burn the same work against a fixed dummy hash, so response timing cannot reveal whether an account exists).
Password reset and exact lookup (additive). UserService gains
get_by_email(email) -> Result<Option<User>> — one bound, byte-exact SQL
equality lookup (WHERE email = ?): no trimming, lowercasing or case
folding, no pagination, no in-memory scan; the returned public model carries
no password hash. It also gains
reset_password_revoking_tokens(email, new_password) -> Result<User>: the
new password is validated (8–128 bytes, else the typed InvalidInput)
and Argon2id-hashed before the database transaction opens, then ONE
transaction performs the exact-email lookup, the password update and the
deletion of every refresh_tokens row of the user. That deletion is
deliberately unscoped by token domain: a password reset is a user-lifecycle
event that must invalidate BOTH the REST refresh tokens and the gRPC
sessions of the account, unlike every domain-scoped sweep above. Any
failure — lookup, update, deletion or commit — rolls the whole transaction
back: the old password keeps authenticating and every credential row
survives. An unknown email returns the typed UserError::NotFoundByEmail
and writes nothing; that typed answer is safe to expose on this
provisioning surface, because unlike the login path it does not have to
hide account existence from callers that explicitly target one account.
gRPC sessions (additive). RefreshTokenService gains add_session,
validate_session, revoke_session, revoke_all_sessions and
cleanup_expired_sessions. The library never generates tokens: the caller
supplies an opaque value that MUST carry at least 256 bits of OS-CSPRNG
unpredictability (empty, oversized or inverted-window values are rejected
before any SQL). Validation is non-consuming — it may repeat per request
and fails closed (None for unknown, expired and revoked alike, with no way
to distinguish them). revoke_session and revoke_all_sessions are single
atomic idempotent statements (logout and logout-everywhere); expired session
rows leave only through the explicit cleanup_expired_sessions sweep.
Token-domain separation. Sessions are stored under the tagged digest
grpc-session:<sha256(raw)> while REST refresh tokens keep their bare
64-hex digests. Every refresh SQL path matches only refresh-domain rows and
every session path only session-domain rows, so no refresh rotation, reuse
purge or deletion can consume or revoke a gRPC session (nor the reverse),
and the same raw value can exist in both domains as two distinct rows.
No SQL migration was needed: the tagged value fits the existing
TEXT NOT NULL UNIQUE column and existing refresh digests stay valid.
Coordinated rollout. Publish 0.4.0 first. Then backend/
(rest_api_server) bumps to it, adapts its email login and re-verifies REST
refresh rotation in its own mini-cycle. Then backend_grpc enables users
and resumes its gated authentication work, consuming
UserService::get_by_email and UserService::reset_password_revoking_tokens
instead of composing a password update with a separate revocation call or
filtering paginated listings in memory. Neither consumer is compatible
with 0.4.0 until its own mini-cycle completes, and deployment waits for
both.
License
Personal use only, non-commercial. See LICENSE.
Dependencies
| ID | Version |
|---|---|
| anyhow | ^1.0.102 |
| argon2 | ^0.5 |
| chrono | ^0.4.44 |
| db_derive | ^1.0.0 |
| include_dir | ^0.7 |
| log | ^0.4.29 |
| password-hash | ^0.5 |
| reqwest | ^0.13.5 |
| serde | ^1.0.214 |
| serde_json | ^1.0.149 |
| sha2 | ^0.10 |
| sqlx | ^0.8.6 |
| thiserror | ^2.0.18 |
| tokio | ^1.50.0 |
| url | ^2 |
| utoipa | ^5.4.0 |