mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 19:06: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
|
# 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
|
## Recommended: Docker services + native kiosk
|
||||||
|
|
||||||
Run server, Angie/nginx, and Node-RED in Docker Compose. Only Angie publishes a
|
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"));
|
const id = Number(getRouterParam(event, "id"));
|
||||||
getCoordinator().sendToKiosk(id, { type: "standby" });
|
getCoordinator().sendToKiosk(id, { type: "standby" });
|
||||||
emitDisplayPower(id, "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}` } });
|
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"));
|
const id = Number(getRouterParam(event, "id"));
|
||||||
getCoordinator().sendToKiosk(id, { type: "wake" });
|
getCoordinator().sendToKiosk(id, { type: "wake" });
|
||||||
emitDisplayPower(id, "on");
|
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}` } });
|
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 { audit } from "../../shared/audit.js";
|
||||||
import { createRateLimiter } from "../../shared/rate-limit.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 {
|
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 ----------------------------------------------------------------
|
// ---- Login ----------------------------------------------------------------
|
||||||
|
|
||||||
app.get("/auth/login", (event) => {
|
app.get("/auth/login", (event) => {
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,6 @@ import { envStr } from "../../shared/env-overrides.js";
|
||||||
import { createRateLimiter } from "../../shared/rate-limit.js";
|
import { createRateLimiter } from "../../shared/rate-limit.js";
|
||||||
import { initMqttBridge, type MqttBridge } from "../../shared/mqtt-bridge.js";
|
import { initMqttBridge, type MqttBridge } from "../../shared/mqtt-bridge.js";
|
||||||
import { createHash } from "node:crypto";
|
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 { Repository } from "../service-store/repository.js";
|
||||||
import type { AuthApi } from "../../shared/auth.js";
|
import type { AuthApi } from "../../shared/auth.js";
|
||||||
import type { SecretsApi } from "../../shared/secrets.js";
|
import type { SecretsApi } from "../../shared/secrets.js";
|
||||||
|
|
@ -203,6 +198,10 @@ function registerPairingRoutes(
|
||||||
secrets: SecretsApi,
|
secrets: SecretsApi,
|
||||||
codeTtl: number,
|
codeTtl: number,
|
||||||
): void {
|
): 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
|
// Kiosk initiates pairing — no auth required
|
||||||
app.post("/api/pair/initiate", async (event) => {
|
app.post("/api/pair/initiate", async (event) => {
|
||||||
const ip = getRequestHeader(event, "x-real-ip")
|
const ip = getRequestHeader(event, "x-real-ip")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue