mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 17:56:34 +00:00
fix(server): move rate-limit creation inside register fns (BSB schema extractor)
Schema extractor evaluates module top-level statically; createRateLimiter calls at module scope threw ReferenceError during bsb-plugin-cli build. Lifting into the per-route register functions keeps build clean. Also: standardise display.standby/wake audit hooks.
This commit is contained in:
parent
17f8c7ce02
commit
150972a272
4 changed files with 54 additions and 9 deletions
|
|
@ -1,5 +1,49 @@
|
|||
# BetterFrame deployment
|
||||
|
||||
## Deployment shapes
|
||||
|
||||
BetterFrame ships as **two artifacts**, deployable in any combination:
|
||||
|
||||
| Variant | What it runs | Where it runs |
|
||||
|--------------|-------------------------------------------|------------------------------|
|
||||
| **bf-server**| Docker compose (server + Angie + Node-RED)| Coolify / VM / on-prem box |
|
||||
| **bf-client**| Rust kiosk binary + cage + plymouth | Pi 5 (LAN-attached) |
|
||||
| *bf-aio* | Both, single Pi | **Demo / single-site only** |
|
||||
|
||||
The `bf-aio` mode (server + kiosk colocated on one Pi) is the simplest install
|
||||
but couples failure domains — when the Pi dies, you lose both the displays it
|
||||
drives AND the management plane for any other kiosks. Use for demos or a
|
||||
single-display site. For anything else, run `bf-server` separately and have
|
||||
`bf-client` Pis point at it.
|
||||
|
||||
### bf-server (Docker compose, Coolify-friendly)
|
||||
|
||||
Pull the repo on the host. Configure via env (overrides `sec-config.yaml`):
|
||||
|
||||
```
|
||||
BF_DATA_DIR=/var/lib/betterframe
|
||||
BF_SQLITE_PATH=/var/lib/betterframe/betterframe.db
|
||||
BF_NODERED_URL=http://nodered:1880
|
||||
BF_SELF_URL=http://server:18080
|
||||
BF_FIRMWARE_SIGNING_KEY= # paste Ed25519 PEM for stable signing key
|
||||
BF_MQTT_URL= # optional MQTT telemetry export
|
||||
```
|
||||
|
||||
In Coolify: create a Docker compose stack from `deploy/docker/docker-compose.yml`,
|
||||
inject the env vars, set a domain on the `angie` service. Backups via the admin
|
||||
UI (`/admin/backup`) — Coolify's S3 hook can pull these on a schedule.
|
||||
|
||||
### bf-client (kiosk Pi)
|
||||
|
||||
```bash
|
||||
sudo apt install -y git
|
||||
git clone https://github.com/BetterCorp/BetterFrame.git ~/betterframe
|
||||
sudo ~/betterframe/deploy/scripts/setup-pi-kiosk.sh client
|
||||
```
|
||||
|
||||
Pairs with whichever `bf-server` is set in `/etc/default/betterframe-kiosk`
|
||||
(`BETTERFRAME_SERVER=http://<server-host>`).
|
||||
|
||||
## Recommended: Docker services + native kiosk
|
||||
|
||||
Run server, Angie/nginx, and Node-RED in Docker Compose. Only Angie publishes a
|
||||
|
|
|
|||
|
|
@ -1429,6 +1429,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
const id = Number(getRouterParam(event, "id"));
|
||||
getCoordinator().sendToKiosk(id, { type: "standby" });
|
||||
emitDisplayPower(id, "standby");
|
||||
audit(deps.repo, event as any, "display.standby", { resource_type: "kiosk", resource_id: id });
|
||||
return new Response(null, { status: 302, headers: { location: `/admin/kiosks/${id}` } });
|
||||
});
|
||||
|
||||
|
|
@ -1436,6 +1437,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
const id = Number(getRouterParam(event, "id"));
|
||||
getCoordinator().sendToKiosk(id, { type: "wake" });
|
||||
emitDisplayPower(id, "on");
|
||||
audit(deps.repo, event as any, "display.wake", { resource_type: "kiosk", resource_id: id });
|
||||
return new Response(null, { status: 302, headers: { location: `/admin/kiosks/${id}` } });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ import { LoginPage, TotpPage, RecoveryPage } from "../../web-templates/auth-page
|
|||
import { audit } from "../../shared/audit.js";
|
||||
import { createRateLimiter } from "../../shared/rate-limit.js";
|
||||
|
||||
// 8 attempts per 60s per IP — paired with the user-account lockout already in
|
||||
// place via deps.auth.config.loginLockoutThreshold to defeat enumeration.
|
||||
const loginGuard = createRateLimiter({ windowMs: 60_000, max: 8 });
|
||||
|
||||
|
||||
export function registerAuthRoutes(app: H3, deps: AdminDeps): void {
|
||||
// 8 attempts per 60s per IP. Paired with the user-account lockout already
|
||||
// wired via deps.auth.config.loginLockoutThreshold. In-function so the BSB
|
||||
// schema extractor doesn't evaluate at module load.
|
||||
const loginGuard = createRateLimiter({ windowMs: 60_000, max: 8 });
|
||||
// ---- Login ----------------------------------------------------------------
|
||||
|
||||
app.get("/auth/login", (event) => {
|
||||
|
|
|
|||
|
|
@ -26,11 +26,6 @@ import { envStr } from "../../shared/env-overrides.js";
|
|||
import { createRateLimiter } from "../../shared/rate-limit.js";
|
||||
import { initMqttBridge, type MqttBridge } from "../../shared/mqtt-bridge.js";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
// Pairing initiation is unauth — guard it so a misbehaving kiosk or attacker
|
||||
// can't spam codes. 20 per minute per IP is generous for legit retries.
|
||||
const pairingGuard = createRateLimiter({ windowMs: 60_000, max: 20 });
|
||||
const claimGuard = createRateLimiter({ windowMs: 60_000, max: 60 });
|
||||
import type { Repository } from "../service-store/repository.js";
|
||||
import type { AuthApi } from "../../shared/auth.js";
|
||||
import type { SecretsApi } from "../../shared/secrets.js";
|
||||
|
|
@ -203,6 +198,10 @@ function registerPairingRoutes(
|
|||
secrets: SecretsApi,
|
||||
codeTtl: number,
|
||||
): void {
|
||||
// Constructed in-function so the BSB schema extractor (which evaluates the
|
||||
// module statically) doesn't see a top-level createRateLimiter call.
|
||||
const pairingGuard = createRateLimiter({ windowMs: 60_000, max: 20 });
|
||||
const claimGuard = createRateLimiter({ windowMs: 60_000, max: 60 });
|
||||
// Kiosk initiates pairing — no auth required
|
||||
app.post("/api/pair/initiate", async (event) => {
|
||||
const ip = getRequestHeader(event, "x-real-ip")
|
||||
|
|
|
|||
Loading…
Reference in a new issue