mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
#1108 — Long-lived camera-stream tokens for HA / Frigate / kiosks. Camera-only V1, hard 365-day cap (no infinite tokens), pbkdf2 hashed at rest, plaintext shown to user exactly once on creation. New "Camera API Tokens" panel under Settings → API Keys with self-service create/revoke, styled confirm modal, admin "All users" view for leak triage. Auth path: /camera/stream tries the existing 60-min ephemeral table first, falls through to the long-lived path. Indexed lookup_prefix keeps verify O(1) per token. Permission audit: gated the existing API-keys-CRUD + Webhook docs + API Browser content behind api_keys:read so non-admins with camera:view land on the API Keys tab and see only the Camera Tokens panel they actually have permission to use. Grid layout collapses to single column for non-admins. Tests: 29 new backend (15 service + 14 integration covering create/list/ revoke ownership rules, the auth fall-through, scope enforcement, prefix collisions) + 6 new frontend tests for the section UI including the new modal flow. All 77 backend tests + 21 frontend camera tests pass. Ruff clean (lint + format). Docs: README updated with fan-out + long-lived-token bullets. Wiki gets a new "Long-Lived Camera Tokens" section under features/camera.md (HA YAML example, security model, permission requirements, revoke flow). Website features.html gets the bullet under Camera Streaming. Also includes #1089 follow-up tweaks already merged in this branch: _stream_start_times.setdefault for accurate stream_uptime, subscribe() RuntimeError retry to close the grace-vs-subscribe race, atomic unsubscribe count via the iter_subscriber on_unsubscribe callback.
This commit is contained in:
@@ -112,6 +112,21 @@ async function request<T>(
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Long-lived camera-stream tokens (#1108). The `token` field is populated
|
||||
// only on the create response — listing endpoints set it to null because
|
||||
// the plaintext value is shown to the user exactly once.
|
||||
export interface LongLivedCameraToken {
|
||||
id: number;
|
||||
user_id: number;
|
||||
name: string;
|
||||
scope: 'camera_stream';
|
||||
lookup_prefix: string;
|
||||
created_at: string;
|
||||
expires_at: string;
|
||||
last_used_at: string | null;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
// Printer types
|
||||
export interface Printer {
|
||||
id: number;
|
||||
@@ -4275,6 +4290,21 @@ export const api = {
|
||||
// Camera
|
||||
getCameraStreamToken: () =>
|
||||
request<{ token: string }>('/printers/camera/stream-token', { method: 'POST' }),
|
||||
|
||||
// Long-lived camera-stream tokens (#1108)
|
||||
createLongLivedCameraToken: (payload: { name: string; expires_in_days: number }) =>
|
||||
request<LongLivedCameraToken>('/auth/tokens', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...payload, scope: 'camera_stream' }),
|
||||
}),
|
||||
listMyLongLivedCameraTokens: () =>
|
||||
request<LongLivedCameraToken[]>('/auth/tokens'),
|
||||
listAllLongLivedCameraTokens: () =>
|
||||
request<LongLivedCameraToken[]>('/auth/tokens/all'),
|
||||
listLongLivedCameraTokensForUser: (userId: number) =>
|
||||
request<LongLivedCameraToken[]>(`/auth/tokens?user_id=${userId}`),
|
||||
revokeLongLivedCameraToken: (tokenId: number) =>
|
||||
request<void>(`/auth/tokens/${tokenId}`, { method: 'DELETE' }),
|
||||
getCameraStreamUrl: (printerId: number, fps = 10) =>
|
||||
withStreamToken(`${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`),
|
||||
getCameraSnapshotUrl: (printerId: number) =>
|
||||
|
||||
Reference in New Issue
Block a user