mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 16:56:33 +00:00
Node-RED nodes (nodered/): - bf-config: shared server URL + admin API key - bf-event-in: filter kiosk events by topic glob - bf-layout-switch: POST display layout-switch - bf-power: kiosk wake/standby - bf-fan: kiosk fan control - bf-cameras: query camera list - Drag-droppable from Node-RED palette Server: - Admin Bearer API key auth on /admin/* (NodeRED can call admin API) - GET /api/admin/cameras for bf-cameras node - Dashboard entity type: - entities.type CHECK adds 'dashboard' - entities.dashboard_id column - shared/nodered-bridge.ts listDashboards() polls /nrdp/flows - Bundle resolves dashboard entity → web cell at /dash/<id> - POST /admin/entities/sync-dashboards mirrors Node-RED tabs - EntitiesPage shows Dashboards section + Sync button - EntityEditPage for dashboard: read-only + "Open in Node-RED" - No create/delete from BF UI — managed in Node-RED - sec-config: noderedUrl on admin-http (was already on api-http)
98 lines
3 KiB
TypeScript
98 lines
3 KiB
TypeScript
/**
|
|
* Auth & setup gate middleware for admin-http.
|
|
*
|
|
* Accepts EITHER a valid session cookie OR an admin-scoped API key in
|
|
* `Authorization: Bearer <bf-...>`. API-key callers get a synthetic User
|
|
* record so downstream handlers (which always read `event.context.user`)
|
|
* keep working unchanged.
|
|
*/
|
|
import { type H3, getCookie, getRequestPath } from "h3";
|
|
import type { AdminDeps } from "./index.js";
|
|
import type { User, Session } from "../../shared/types.js";
|
|
|
|
declare module "h3" {
|
|
interface H3EventContext {
|
|
user?: User;
|
|
session?: Session;
|
|
apiKeyPrefix?: string;
|
|
}
|
|
}
|
|
|
|
function syntheticApiKeyUser(keyPrefix: string): User {
|
|
return {
|
|
id: 0,
|
|
username: `api:${keyPrefix}`,
|
|
password_hash: "",
|
|
role: "admin",
|
|
is_active: true,
|
|
totp_enabled: false,
|
|
totp_secret_encrypted: null,
|
|
recovery_codes_hashed: [],
|
|
must_change_password: false,
|
|
failed_login_count: 0,
|
|
locked_until: null,
|
|
last_login_at: null,
|
|
created_at: new Date(0).toISOString(),
|
|
};
|
|
}
|
|
|
|
export function registerMiddleware(app: H3, deps: AdminDeps): void {
|
|
app.use(async (event) => {
|
|
const path = getRequestPath(event);
|
|
|
|
if (
|
|
path === "/setup" ||
|
|
path.startsWith("/static/") ||
|
|
path === "/healthz" ||
|
|
path === "/readyz" ||
|
|
path === "/version" ||
|
|
path === "/api/admin/_check" ||
|
|
path === "/"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (!deps.repo.isSetupComplete()) {
|
|
if (!path.startsWith("/auth/")) {
|
|
return new Response(null, { status: 302, headers: { location: "/setup" } });
|
|
}
|
|
}
|
|
|
|
if (path.startsWith("/auth/")) {
|
|
return;
|
|
}
|
|
|
|
if (path.startsWith("/admin") || path.startsWith("/api/admin")) {
|
|
// ---- Bearer API key (admin scope) -------------------------------------
|
|
// Lets Node-RED nodes + scripted automation hit /admin/* without owning
|
|
// a session cookie. Must come BEFORE the cookie redirect so a missing
|
|
// cookie + present API key doesn't 302 to /auth/login.
|
|
const authz = event.req.headers.get("authorization");
|
|
if (authz && authz.startsWith("Bearer ")) {
|
|
const token = authz.slice(7);
|
|
const key = await deps.auth.verifyApiKey(token, event.req.headers.get("x-real-ip"));
|
|
if (!key || !key.scopes.includes("admin")) {
|
|
return new Response(null, { status: 401 });
|
|
}
|
|
event.context.user = syntheticApiKeyUser(key.key_prefix);
|
|
event.context.apiKeyPrefix = key.key_prefix;
|
|
return;
|
|
}
|
|
|
|
const cookie = getCookie(event, deps.cookieName);
|
|
if (!cookie) {
|
|
return new Response(null, { status: 302, headers: { location: "/auth/login" } });
|
|
}
|
|
const resolved = deps.auth.resolveSession(cookie);
|
|
if (!resolved) {
|
|
return new Response(null, { status: 302, headers: { location: "/auth/login" } });
|
|
}
|
|
if (resolved.session.totp_pending) {
|
|
return new Response(null, { status: 302, headers: { location: "/auth/totp" } });
|
|
}
|
|
event.context.user = resolved.user;
|
|
event.context.session = resolved.session;
|
|
return;
|
|
}
|
|
});
|
|
}
|