mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 18:47:13 +02:00
fix(auth): let API keys manage projects via new can_manage_projects scope (#1893)
PROJECTS_CREATE/UPDATE/DELETE were in _APIKEY_DENIED_PERMISSIONS with no entry in _APIKEY_SCOPE_BY_PERMISSION, so every project mutation returned a generic 403 for any API key regardless of granted permissions -- the same regression class as archives (#1888) and library (#1832). Add a per-key can_manage_projects scope. Project routes gate on plain PROJECTS_* (no OWN/ALL split), so all three CRUD permissions map to the one scope; membership edits (add-archives) gate on PROJECTS_UPDATE and are covered. PROJECTS_READ is unchanged (already under can_read_status). Column defaults TRUE for new keys; existing rows backfill to FALSE so the upgrade never silently widens scope. Migration is BOOLEAN (SQLite + Postgres safe), verified on fresh SQLite and Postgres 17. Bundled SpoolBuddy kiosk key set to False. Settings API-key UI gets a Manage Projects toggle + Projects badge; 11-locale i18n. RBAC scope matrix + drift guards extended.
This commit is contained in:
@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [0.2.5b2] - Unreleased
|
||||
|
||||
### Fixed
|
||||
- **API keys could not manage Projects — every project mutation returned `403 "API keys cannot be used for administrative operations"` regardless of the key's granted permissions (#1893, reporter @abbasegbeyemi)** — `POST /projects/{id}/add-archives`, project create/update/delete, and every other project mutation route was unreachable for any API key. **Root cause.** `PROJECTS_CREATE` / `PROJECTS_UPDATE` / `PROJECTS_DELETE` were in `_APIKEY_DENIED_PERMISSIONS` in `core/auth.py` with no corresponding entry in `_APIKEY_SCOPE_BY_PERMISSION` and no `can_manage_projects` flag on `api_keys` at all — so under the GHSA-r2qv allowlist model they resolved to scope `None` and raised the generic administrative-operations 403. This is the exact regression class already fixed for archives (#1888) and library (#1832): the projects block sat directly between the comment blocks documenting those two carve-outs but was never itself carved out. **Fix.** New per-key scope `can_manage_projects` (column on `api_keys`, DEFAULT TRUE for keys created via the UI going forward; existing rows backfill to FALSE so the upgrade path never silently widens scope — these permissions were explicitly denied for every key before, so nothing relies on them). Unlike archives/library, the project routes gate on plain `RequirePermissionIfAuthEnabled(Permission.PROJECTS_*)` — there is no OWN/ALL ownership split for projects — so all three CRUD permissions map directly to the one scope. Project **membership** edits (`add_archives_to_project` etc.) gate on `PROJECTS_UPDATE`, so they're covered by the same toggle; `PROJECTS_READ` is unchanged (already under `can_read_status`, so API keys could always read projects). Users opt a key in from Settings → API Keys ("Manage Projects" toggle, with a "Projects" badge on the key list). The bundled SpoolBuddy kiosk key (created via the CLI) is set to `can_manage_projects=False` to stay minimally scoped. **Migration** is dialect-agnostic (`BOOLEAN` is valid on both SQLite and Postgres); verified end-to-end on a throwaway fresh SQLite and Postgres 17 that the column adds, legacy rows backfill to FALSE, and a new row defaults to TRUE. **Tests.** `test_auth_apikey_rbac.py` extended: the `_check_apikey_permissions` scope matrix now covers all three project permissions (true→allow, false→403, no cross-scope leakage), and `PROJECTS_CREATE` / `_UPDATE` / `_DELETE` added to the operational-allowed drift guard + threaded through the structural allowlist/flag-parity checks — 63 cases green. **Scope.** Backend (model + migration + allowlist + schema + route + CLI) plus the Settings API-key UI (toggle + badge + type) and 11-locale i18n for the new label/description/badge. No change to the project routes themselves — they already gated on the right permissions; only the API-key classification of those permissions was wrong.
|
||||
- **Auto-drying stopped a manually started AMS drying cycle after exactly 30 minutes, cutting long PETG/PA dries short (#1892, reporter @Spionkiller01)** — With ambient/queue auto-drying enabled, starting a drying cycle *manually on the printer* (or a cycle that survived a Bambuddy restart) got a stop-drying MQTT command ~30 minutes in, every time — killing an intended 8-12 h cycle. The reporter had Bambu Lab support analyse the printer logs, which confirmed an external tool issued the stop; that tool was Bambuddy. **Root cause — two defects compounding in `_check_auto_drying()` (`backend/app/services/print_scheduler.py`).** (1) The already-drying branch carried the comment *"Drying we didn't start (manual or from before restart) — track but don't stop"* but the very next lines applied the humidity-based auto-stop to it anyway; a manually started dry was treated identically to a Bambuddy-initiated one. (2) The humidity re-check is fundamentally unreliable: relative humidity drops steeply in heated air, so the AMS sensor reads ~15-20% within minutes of the dryer starting even while the filament is still saturated (the reporter's log shows 18%). So `humidity <= threshold` is effectively *always true* once drying runs, and the only thing delaying the stop was the `_min_drying_seconds = 1800` floor — which is why the kill landed at exactly the 30-minute mark. This second defect also silently truncated Bambuddy's **own** preset-duration dries (e.g. a PETG 8 h cycle) to ~30 min, not just manual ones. **Fix.** Removed the humidity-based early-stop entirely — a running drying cycle is now left to run to its configured duration, which the firmware stops when the duration elapses. This is simpler and more correct than exempting only manual dries (the reporter's suggested `_manual_drying` set), because the humidity re-check can't distinguish "filament is dry" from "air is hot" for *any* cycle, so it never did its intended job — it just always fired at the floor. Scheduling-driven stops are unaffected and still work through `_stop_drying()`: a print taking priority, or queue-mode no longer needing the dry, still stops it. The now-unused `_min_drying_seconds` attribute was removed. Bambuddy still *starts* auto-drying on the same humidity-over-threshold trigger; only the mid-cycle humidity re-stop is gone. **Tests.** `test_scheduler_auto_drying.py` updated: `TestMinimumDryingTime` now pins the #1892 contract (a running dry is never stopped by a humidity re-check — before or long after the old floor, including when humidity reads low), and `TestBlockForDryingBugFix` asserts an already-running dry in block mode is left alone (block mode still gates *new* starts on printers with pending items). 51/51 in the file green, ruff clean. **Scope.** Backend-only, one branch simplified in `_check_auto_drying` plus the attribute removal. No DB migration, no new permission, no i18n key, no frontend change. Users on 0.2.5b1 and earlier: the fix takes effect on the next Bambuddy restart. **Workaround for older versions:** disable ambient/queue auto-drying in Settings before starting a manual drying cycle.
|
||||
- **When auth was enabled, a browser that could not mint a WebSocket token retried forever, hammering `POST /api/v1/auth/ws-token` every 3 seconds (reported by corporate sponsor)** — After the GHSA-r2qv hardening (`b7d7c825`), `/api/v1/ws` requires a token from `POST /api/v1/auth/ws-token` (gated by `Permission.WEBSOCKET_CONNECT`). If that mint failed, `useWebSocket` swallowed the error and **fell through to open a tokenless socket anyway**; the server closed it with code `4401`, and `ws.onclose` unconditionally scheduled a reconnect 3 s later — an endless loop that spammed the auth endpoint with `401`/`403`s. The dominant trigger was a *validly logged-in* user whose group lacks `WEBSOCKET_CONNECT` (e.g. a custom lower-privilege "ops" group): the mint returns `403` and the loop never ends. A secondary leak: on logout the provider unmounts, but the `close()`-triggered `onclose` could still schedule one more post-unmount reconnect. **Fix.** The token-mint failure is now classified: a `401` (JWT expired — `request()` already clears it and dispatches `auth:expired`, redirecting to `/login`) or `403` (valid session, missing permission — stays logged in, live updates degrade to the existing REST polling) **stops** the hook — no tokenless socket, no reconnect. A `4401` close is now treated as terminal (reconnecting can't fix an auth rejection without a fresh login, which remounts the provider). Network/`5xx` errors still reconnect as before. A `disposedRef` set in the effect cleanup before `close()` prevents the unmount-race reconnect. The same 401/403 no-open guard was applied to `StreamOverlayPage` (which has no reconnect loop, so it was one doomed socket per mount rather than a spin). **Tests.** New `useWebSocket` cases: a `4401` close does not reconnect, a `403` mint opens no socket and does not loop, and a close firing during unmount schedules no reconnect. **Note.** Auto-granting `WEBSOCKET_CONNECT` to lesser groups was rejected on purpose — the WebSocket streams all printer status, so that would partly undo the GHSA-r2qv gate; graceful degradation is the correct behavior. The group editor now shows a one-line hint under the WebSocket permission explaining that live updates require it (falls back to polling otherwise), translated across all 11 locales.
|
||||
- **A transient error while validating a stored login on page load could silently discard a valid "Remember Me" token, forcing a re-login (#1889, reporter @superdong69)** — On mount, `AuthContext.checkAuthStatus` restores the persisted token from `localStorage` and validates it via `GET /auth/me`. The `catch` around that call cleared the token on **any** failure — not just a definitive `401` invalid-token — so a brief backend-not-ready-yet or reverse-proxy hiccup (plausible right after a container restart, e.g. on Unraid) would delete a still-valid token. Because the token was *deleted*, a subsequent reload couldn't recover it either; the user was bounced to the login screen. **Fix.** Token validation now retries transient failures (up to 3 attempts with short backoff) and only discards the token on a definitive `401` invalid-token response — which `request()` already handles (it clears the token and dispatches `auth:expired`). Transient/`5xx`/network errors leave the persisted token intact so the session survives a slow load and a reload can recover. Note "Remember Me" remains client-storage only (localStorage vs sessionStorage); it does not extend the server-side JWT lifetime, which stays governed by `session_max_hours` (default 24h). **Tests.** New `AuthContext` cases: `/auth/me` failing transiently keeps the persisted token (no forced re-login), a definitive `401` clears it, and a valid token loads the user. **Note.** This hardens a real latent bug; if a specific report still recurs, the likely remaining cause is the browser clearing site data / DOM storage on close (e.g. Firefox "Delete cookies and site data when Firefox is closed", strict tracking protection, or a private window), which no server-side change can prevent.
|
||||
|
||||
@@ -67,6 +67,7 @@ async def create_api_key(
|
||||
can_manage_inventory=data.can_manage_inventory,
|
||||
can_manage_maintenance=data.can_manage_maintenance,
|
||||
can_manage_archives=data.can_manage_archives,
|
||||
can_manage_projects=data.can_manage_projects,
|
||||
can_access_cloud=data.can_access_cloud,
|
||||
can_update_energy_cost=data.can_update_energy_cost,
|
||||
printer_ids=data.printer_ids,
|
||||
@@ -90,6 +91,7 @@ async def create_api_key(
|
||||
can_manage_inventory=api_key.can_manage_inventory,
|
||||
can_manage_maintenance=api_key.can_manage_maintenance,
|
||||
can_manage_archives=api_key.can_manage_archives,
|
||||
can_manage_projects=api_key.can_manage_projects,
|
||||
can_access_cloud=api_key.can_access_cloud,
|
||||
can_update_energy_cost=api_key.can_update_energy_cost,
|
||||
printer_ids=api_key.printer_ids,
|
||||
@@ -147,6 +149,8 @@ async def update_api_key(
|
||||
api_key.can_manage_maintenance = data.can_manage_maintenance
|
||||
if data.can_manage_archives is not None:
|
||||
api_key.can_manage_archives = data.can_manage_archives
|
||||
if data.can_manage_projects is not None:
|
||||
api_key.can_manage_projects = data.can_manage_projects
|
||||
if data.can_access_cloud is not None:
|
||||
# Same constraint as create — flipping cloud access on a legacy key
|
||||
# without an owner would be silently broken; reject at the route layer.
|
||||
|
||||
@@ -76,6 +76,8 @@ async def kiosk_bootstrap(
|
||||
# Kiosk doesn't manage print archives either — keep it minimally
|
||||
# scoped (#1888).
|
||||
can_manage_archives=False,
|
||||
# Kiosk doesn't manage projects — keep it minimally scoped (#1893).
|
||||
can_manage_projects=False,
|
||||
printer_ids=None,
|
||||
enabled=True,
|
||||
expires_at=None,
|
||||
|
||||
@@ -167,6 +167,17 @@ _APIKEY_SCOPE_BY_PERMISSION: dict[Permission, str] = {
|
||||
Permission.ARCHIVES_UPDATE_ALL: "can_manage_archives",
|
||||
Permission.ARCHIVES_DELETE_OWN: "can_manage_archives",
|
||||
Permission.ARCHIVES_DELETE_ALL: "can_manage_archives",
|
||||
# can_manage_projects — project curation. Carved out of the admin denylist
|
||||
# so automations can create projects and batch-add archives via API key
|
||||
# (#1893). The project mutation routes gate on plain
|
||||
# ``RequirePermissionIfAuthEnabled(Permission.PROJECTS_*)`` (no OWN/ALL
|
||||
# ownership split — projects have no per-row ownership permission), so the
|
||||
# three CRUD permissions map directly to the one scope. Membership edits
|
||||
# (e.g. add-archives-to-project) gate on PROJECTS_UPDATE, so they're covered.
|
||||
# PROJECTS_READ stays under can_read_status (unchanged).
|
||||
Permission.PROJECTS_CREATE: "can_manage_projects",
|
||||
Permission.PROJECTS_UPDATE: "can_manage_projects",
|
||||
Permission.PROJECTS_DELETE: "can_manage_projects",
|
||||
# can_access_cloud — narrow opt-in scope, gated by the router-level
|
||||
# ``_cloud_api_key_gate`` and additionally enforced here so the route-
|
||||
# level ``cloud_caller(Permission.CLOUD_AUTH)`` dep also fails closed
|
||||
@@ -228,9 +239,10 @@ _APIKEY_DENIED_PERMISSIONS: frozenset[Permission] = frozenset(
|
||||
# `require_ownership_permission`. Purge stays denied as a genuinely
|
||||
# destructive op.
|
||||
Permission.LIBRARY_PURGE,
|
||||
Permission.PROJECTS_CREATE,
|
||||
Permission.PROJECTS_UPDATE,
|
||||
Permission.PROJECTS_DELETE,
|
||||
# PROJECTS_CREATE / _UPDATE / _DELETE moved to the allowlist under
|
||||
# `can_manage_projects` (#1893) — they were denied for every API key,
|
||||
# making the project-management surface (create, add-archives, delete)
|
||||
# unreachable, same regression class as the archives/library carve-outs.
|
||||
Permission.FILAMENTS_CREATE,
|
||||
Permission.FILAMENTS_UPDATE,
|
||||
Permission.FILAMENTS_DELETE,
|
||||
|
||||
@@ -2561,6 +2561,25 @@ async def run_migrations(conn):
|
||||
async with conn.begin_nested():
|
||||
await conn.execute(text("UPDATE api_keys SET can_manage_archives = FALSE"))
|
||||
|
||||
# #1893: carve project CRUD + membership (create/update/delete, add-archives)
|
||||
# out of the admin denylist so automations can manage projects via API key.
|
||||
# Identical shape and reasoning to can_manage_archives above: PROJECTS_CREATE
|
||||
# / _UPDATE / _DELETE were EXPLICITLY denied for every API key under the
|
||||
# pre-migration model (they were on ``_APIKEY_DENIED_PERMISSIONS``), so no
|
||||
# existing integration relies on them. Column default TRUE for keys created
|
||||
# via the UI going forward; existing rows backfill to FALSE so the upgrade
|
||||
# path does not silently widen scope for keys created before this flag
|
||||
# existed. Users opt in via Settings → API Keys per key. BOOLEAN is valid on
|
||||
# both SQLite and Postgres, so no dialect branch is needed.
|
||||
column_existed = await _api_keys_column_exists(conn, "can_manage_projects")
|
||||
await _safe_execute(
|
||||
conn,
|
||||
"ALTER TABLE api_keys ADD COLUMN can_manage_projects BOOLEAN DEFAULT TRUE",
|
||||
)
|
||||
if not column_existed:
|
||||
async with conn.begin_nested():
|
||||
await conn.execute(text("UPDATE api_keys SET can_manage_projects = FALSE"))
|
||||
|
||||
# Migration: Soft-delete column for trash bin (Issue #1008). Indexed so the
|
||||
# sweeper's "SELECT ... WHERE deleted_at < cutoff" and the trash list's
|
||||
# "WHERE deleted_at IS NOT NULL" stay cheap as the table grows.
|
||||
|
||||
@@ -42,6 +42,9 @@ class APIKey(Base):
|
||||
can_manage_archives: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True
|
||||
) # Create/update/delete print archives (not purge) (#1888)
|
||||
can_manage_projects: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True
|
||||
) # Create/update/delete projects + manage membership (add archives) (#1893)
|
||||
can_access_cloud: Mapped[bool] = mapped_column(Boolean, default=False) # Read /cloud/* on the owner's behalf
|
||||
# Narrowly-scoped settings write: only POST /settings/electricity-price.
|
||||
# Lets HA/Tibber-style automations push dynamic tariff updates without
|
||||
|
||||
@@ -16,6 +16,7 @@ class APIKeyCreate(BaseModel):
|
||||
True # Log/reset maintenance items, edit intervals, manage type catalog (#1832 follow-up)
|
||||
)
|
||||
can_manage_archives: bool = True # Create/update/delete print archives — not purge (#1888)
|
||||
can_manage_projects: bool = True # Create/update/delete projects + membership (add archives) (#1893)
|
||||
can_access_cloud: bool = False # Read /cloud/* on the creator's behalf — default off (#1182)
|
||||
can_update_energy_cost: bool = False # POST /settings/electricity-price only (#1356)
|
||||
printer_ids: list[int] | None = None # null = all printers
|
||||
@@ -33,6 +34,7 @@ class APIKeyUpdate(BaseModel):
|
||||
can_manage_inventory: bool | None = None
|
||||
can_manage_maintenance: bool | None = None
|
||||
can_manage_archives: bool | None = None
|
||||
can_manage_projects: bool | None = None
|
||||
can_access_cloud: bool | None = None
|
||||
can_update_energy_cost: bool | None = None
|
||||
printer_ids: list[int] | None = None
|
||||
@@ -54,6 +56,7 @@ class APIKeyResponse(BaseModel):
|
||||
can_manage_inventory: bool
|
||||
can_manage_maintenance: bool
|
||||
can_manage_archives: bool
|
||||
can_manage_projects: bool
|
||||
can_access_cloud: bool
|
||||
can_update_energy_cost: bool
|
||||
printer_ids: list[int] | None
|
||||
|
||||
@@ -162,6 +162,11 @@ class TestApiKeyDenylistIntegrity:
|
||||
# can_manage_archives allowlist scope; must not be denied.
|
||||
Permission.ARCHIVES_DELETE_ALL,
|
||||
Permission.ARCHIVES_UPDATE_ALL,
|
||||
# #1893: project CRUD moved off the denylist to the
|
||||
# can_manage_projects allowlist scope; must not be denied.
|
||||
Permission.PROJECTS_CREATE,
|
||||
Permission.PROJECTS_UPDATE,
|
||||
Permission.PROJECTS_DELETE,
|
||||
# SpoolBuddy kiosk reads settings (e.g. language) via API key — must stay allowed.
|
||||
Permission.SETTINGS_READ,
|
||||
}
|
||||
@@ -221,6 +226,7 @@ class TestApiKeyScopeAllowlist:
|
||||
"can_manage_inventory",
|
||||
"can_manage_maintenance",
|
||||
"can_manage_archives",
|
||||
"can_manage_projects",
|
||||
"can_access_cloud",
|
||||
}
|
||||
used_flags = set(_APIKEY_SCOPE_BY_PERMISSION.values())
|
||||
@@ -249,6 +255,7 @@ class TestApiKeyScopeAllowlist:
|
||||
"can_manage_inventory",
|
||||
"can_manage_maintenance",
|
||||
"can_manage_archives",
|
||||
"can_manage_projects",
|
||||
"can_access_cloud",
|
||||
],
|
||||
)
|
||||
@@ -278,6 +285,7 @@ class _FakeApiKey:
|
||||
can_manage_inventory=False,
|
||||
can_manage_maintenance=False,
|
||||
can_manage_archives=False,
|
||||
can_manage_projects=False,
|
||||
):
|
||||
self.can_read_status = can_read_status
|
||||
self.can_queue = can_queue
|
||||
@@ -286,6 +294,7 @@ class _FakeApiKey:
|
||||
self.can_manage_inventory = can_manage_inventory
|
||||
self.can_manage_maintenance = can_manage_maintenance
|
||||
self.can_manage_archives = can_manage_archives
|
||||
self.can_manage_projects = can_manage_projects
|
||||
|
||||
|
||||
class TestCheckApiKeyPermissionsMatrix:
|
||||
@@ -341,6 +350,13 @@ class TestCheckApiKeyPermissionsMatrix:
|
||||
("ARCHIVES_UPDATE_ALL", "can_manage_archives", "edit any archive"),
|
||||
("ARCHIVES_DELETE_OWN", "can_manage_archives", "delete own archive"),
|
||||
("ARCHIVES_DELETE_ALL", "can_manage_archives", "delete any archive"),
|
||||
# can_manage_projects (#1893) — project CRUD + membership via API key.
|
||||
# Projects gate on plain PROJECTS_* (no OWN/ALL split), so the three
|
||||
# permissions map directly to the one scope. PROJECTS_READ stays under
|
||||
# can_read_status. Membership edits (add-archives) gate on PROJECTS_UPDATE.
|
||||
("PROJECTS_CREATE", "can_manage_projects", "create a project"),
|
||||
("PROJECTS_UPDATE", "can_manage_projects", "update a project / add archives"),
|
||||
("PROJECTS_DELETE", "can_manage_projects", "delete a project"),
|
||||
]
|
||||
|
||||
_ADMIN_CASES = [
|
||||
@@ -392,6 +408,7 @@ class TestCheckApiKeyPermissionsMatrix:
|
||||
"can_manage_inventory",
|
||||
"can_manage_maintenance",
|
||||
"can_manage_archives",
|
||||
"can_manage_projects",
|
||||
)
|
||||
if f != required_flag
|
||||
}
|
||||
|
||||
@@ -1050,6 +1050,7 @@ export interface APIKey {
|
||||
can_manage_inventory: boolean;
|
||||
can_manage_maintenance: boolean;
|
||||
can_manage_archives: boolean;
|
||||
can_manage_projects: boolean;
|
||||
can_access_cloud: boolean;
|
||||
can_update_energy_cost: boolean;
|
||||
printer_ids: number[] | null;
|
||||
@@ -1068,6 +1069,7 @@ export interface APIKeyCreate {
|
||||
can_manage_inventory?: boolean;
|
||||
can_manage_maintenance?: boolean;
|
||||
can_manage_archives?: boolean;
|
||||
can_manage_projects?: boolean;
|
||||
can_access_cloud?: boolean;
|
||||
can_update_energy_cost?: boolean;
|
||||
printer_ids?: number[] | null;
|
||||
@@ -1087,6 +1089,7 @@ export interface APIKeyUpdate {
|
||||
can_manage_inventory?: boolean;
|
||||
can_manage_maintenance?: boolean;
|
||||
can_manage_archives?: boolean;
|
||||
can_manage_projects?: boolean;
|
||||
can_access_cloud?: boolean;
|
||||
can_update_energy_cost?: boolean;
|
||||
printer_ids?: number[] | null;
|
||||
|
||||
@@ -1992,8 +1992,11 @@ export default {
|
||||
manageMaintenanceDescription: 'Abgeschlossene Wartungen protokollieren, Zähler zurücksetzen, Intervalle bearbeiten und den Wartungstyp-Katalog verwalten. Geeignet für Home-Assistant-Automatisierungen, die „Düse gereinigt" protokollieren, ohne umfassendere Druckersteuerung zu gewähren.',
|
||||
manageArchives: 'Archive verwalten',
|
||||
manageArchivesDescription: 'Druckarchive bearbeiten und löschen, einschließlich des Entfernens alter Drucke. Umfasst nicht das Bereinigen ihres Statistikbeitrags. Geeignet für Automatisierungen, die den Druckverlauf ausdünnen.',
|
||||
manageProjects: 'Projekte verwalten',
|
||||
manageProjectsDescription: 'Projekte erstellen, aktualisieren und löschen sowie Archive zu ihnen hinzufügen. Geeignet für Automatisierungen, die Drucke in Projekten organisieren.',
|
||||
maintenanceBadge: 'Wartung',
|
||||
archivesBadge: 'Archive',
|
||||
projectsBadge: 'Projekte',
|
||||
libraryBadge: 'Bibliothek',
|
||||
inventoryBadge: 'Bestand',
|
||||
cloudAccess: 'Cloud-Zugriff erlauben',
|
||||
|
||||
@@ -2011,10 +2011,13 @@ export default {
|
||||
manageMaintenanceDescription: 'Log completed maintenance, reset counters, edit intervals, and manage the maintenance-type catalog. Suited to Home Assistant automations that record "I cleaned the nozzle" without granting broader printer control.',
|
||||
manageArchives: 'Manage Archives',
|
||||
manageArchivesDescription: 'Edit and delete print archives, including removing old prints. Does not include purging their statistics contribution. Suited to automations that prune the print history.',
|
||||
manageProjects: 'Manage Projects',
|
||||
manageProjectsDescription: 'Create, update, and delete projects, and add archives to them. Suited to automations that organize prints into projects.',
|
||||
libraryBadge: 'Library',
|
||||
inventoryBadge: 'Inventory',
|
||||
maintenanceBadge: 'Maintenance',
|
||||
archivesBadge: 'Archives',
|
||||
projectsBadge: 'Projects',
|
||||
cloudAccess: 'Allow cloud access',
|
||||
cloudAccessDescription: 'Read Bambu Cloud presets and filaments on your behalf. Requires you to be signed into Bambu Cloud.',
|
||||
cloudBadge: 'Cloud',
|
||||
|
||||
@@ -1995,8 +1995,11 @@ export default {
|
||||
manageMaintenanceDescription: 'Registrar mantenimientos completados, restablecer contadores, editar intervalos y gestionar el catálogo de tipos de mantenimiento. Ideal para automatizaciones de Home Assistant que registran "limpié la boquilla" sin conceder un control más amplio de la impresora.',
|
||||
manageArchives: 'Gestionar archivos de impresión',
|
||||
manageArchivesDescription: 'Editar y eliminar archivos de impresión, incluida la eliminación de impresiones antiguas. No incluye la purga de su contribución a las estadísticas. Ideal para automatizaciones que depuran el historial de impresión.',
|
||||
manageProjects: 'Gestionar proyectos',
|
||||
manageProjectsDescription: 'Crear, actualizar y eliminar proyectos, y añadirles archivos de impresión. Ideal para automatizaciones que organizan las impresiones en proyectos.',
|
||||
maintenanceBadge: 'Mantenimiento',
|
||||
archivesBadge: 'Archivos',
|
||||
projectsBadge: 'Proyectos',
|
||||
libraryBadge: 'Biblioteca',
|
||||
inventoryBadge: 'Inventario',
|
||||
cloudAccess: 'Permitir el acceso a la nube',
|
||||
|
||||
@@ -1948,8 +1948,11 @@ export default {
|
||||
manageMaintenanceDescription: 'Enregistrer les maintenances effectuées, réinitialiser les compteurs, modifier les intervalles et gérer le catalogue des types de maintenance. Adapté aux automatisations Home Assistant qui consignent "J\'ai nettoyé la buse" sans accorder un contrôle plus large de l\'imprimante.',
|
||||
manageArchives: 'Gérer les archives',
|
||||
manageArchivesDescription: 'Modifier et supprimer les archives d\'impression, y compris la suppression des anciennes impressions. N\'inclut pas la purge de leur contribution aux statistiques. Adapté aux automatisations qui élaguent l\'historique d\'impression.',
|
||||
manageProjects: 'Gérer les projets',
|
||||
manageProjectsDescription: 'Créer, modifier et supprimer des projets, et y ajouter des archives. Adapté aux automatisations qui organisent les impressions en projets.',
|
||||
maintenanceBadge: 'Maintenance',
|
||||
archivesBadge: 'Archives',
|
||||
projectsBadge: 'Projets',
|
||||
libraryBadge: 'Bibliothèque',
|
||||
inventoryBadge: 'Inventaire',
|
||||
cloudAccess: 'Autoriser l\'accès cloud',
|
||||
|
||||
@@ -1948,8 +1948,11 @@ export default {
|
||||
manageMaintenanceDescription: 'Registra le manutenzioni completate, azzera i contatori, modifica gli intervalli e gestisci il catalogo dei tipi di manutenzione. Adatto alle automazioni Home Assistant che registrano "ho pulito l\'ugello" senza concedere un controllo più ampio della stampante.',
|
||||
manageArchives: 'Gestisci archivi',
|
||||
manageArchivesDescription: 'Modifica ed elimina gli archivi di stampa, inclusa la rimozione delle vecchie stampe. Non include l\'eliminazione del loro contributo alle statistiche. Adatto alle automazioni che sfoltiscono la cronologia di stampa.',
|
||||
manageProjects: 'Gestisci progetti',
|
||||
manageProjectsDescription: 'Crea, aggiorna ed elimina progetti e aggiungi archivi ad essi. Adatto alle automazioni che organizzano le stampe in progetti.',
|
||||
maintenanceBadge: 'Manutenzione',
|
||||
archivesBadge: 'Archivi',
|
||||
projectsBadge: 'Progetti',
|
||||
libraryBadge: 'Libreria',
|
||||
inventoryBadge: 'Inventario',
|
||||
cloudAccess: 'Consenti accesso cloud',
|
||||
|
||||
@@ -1991,8 +1991,11 @@ export default {
|
||||
manageMaintenanceDescription: '完了したメンテナンスの記録、カウンターのリセット、間隔の編集、メンテナンス種類カタログの管理。より広範なプリンター制御を許可することなく「ノズルをクリーニングした」を記録する Home Assistant オートメーションに適しています。',
|
||||
manageArchives: 'アーカイブの管理',
|
||||
manageArchivesDescription: '古い印刷の削除を含む、印刷アーカイブの編集と削除。統計への寄与の削除は含まれません。印刷履歴を整理するオートメーションに適しています。',
|
||||
manageProjects: 'プロジェクトの管理',
|
||||
manageProjectsDescription: 'プロジェクトの作成、更新、削除、およびプロジェクトへのアーカイブの追加。印刷をプロジェクトに整理するオートメーションに適しています。',
|
||||
maintenanceBadge: 'メンテナンス',
|
||||
archivesBadge: 'アーカイブ',
|
||||
projectsBadge: 'プロジェクト',
|
||||
libraryBadge: 'ライブラリ',
|
||||
inventoryBadge: '在庫',
|
||||
cloudAccess: 'クラウドアクセスを許可',
|
||||
|
||||
@@ -1877,8 +1877,11 @@ export default {
|
||||
manageMaintenanceDescription: '완료된 유지보수 기록, 카운터 재설정, 주기 편집, 유지보수 유형 카탈로그 관리. 프린터 제어를 더 광범위하게 허용하지 않고 "노즐을 청소했다"를 기록하는 Home Assistant 자동화에 적합합니다.',
|
||||
manageArchives: '아카이브 관리',
|
||||
manageArchivesDescription: '오래된 출력물 제거를 포함하여 출력 아카이브를 편집하고 삭제합니다. 통계 기여 항목의 제거는 포함되지 않습니다. 출력 기록을 정리하는 자동화에 적합합니다.',
|
||||
manageProjects: '프로젝트 관리',
|
||||
manageProjectsDescription: '프로젝트를 생성, 업데이트, 삭제하고 프로젝트에 아카이브를 추가합니다. 출력물을 프로젝트로 정리하는 자동화에 적합합니다.',
|
||||
maintenanceBadge: '유지보수',
|
||||
archivesBadge: '아카이브',
|
||||
projectsBadge: '프로젝트',
|
||||
libraryBadge: '라이브러리',
|
||||
inventoryBadge: '재고',
|
||||
cloudAccess: '클라우드 접근 허용',
|
||||
|
||||
@@ -1948,8 +1948,11 @@ export default {
|
||||
manageMaintenanceDescription: 'Registrar manutenções concluídas, redefinir contadores, editar intervalos e gerenciar o catálogo de tipos de manutenção. Adequado a automações do Home Assistant que registram "limpei o bico" sem conceder controle mais amplo da impressora.',
|
||||
manageArchives: 'Gerenciar arquivos de impressão',
|
||||
manageArchivesDescription: 'Editar e excluir arquivos de impressão, incluindo a remoção de impressões antigas. Não inclui a limpeza da contribuição deles para as estatísticas. Adequado a automações que reduzem o histórico de impressão.',
|
||||
manageProjects: 'Gerenciar projetos',
|
||||
manageProjectsDescription: 'Criar, atualizar e excluir projetos e adicionar arquivos de impressão a eles. Adequado a automações que organizam as impressões em projetos.',
|
||||
maintenanceBadge: 'Manutenção',
|
||||
archivesBadge: 'Arquivos',
|
||||
projectsBadge: 'Projetos',
|
||||
libraryBadge: 'Biblioteca',
|
||||
inventoryBadge: 'Estoque',
|
||||
cloudAccess: 'Permitir acesso à nuvem',
|
||||
|
||||
@@ -1996,8 +1996,11 @@ export default {
|
||||
manageMaintenanceDescription: 'Tamamlanan bakımları kaydet, sayaçları sıfırla, aralıkları düzenle ve bakım türü kataloğunu yönet. Daha geniş yazıcı kontrolü vermeden "nozulu temizledim"i kaydeden Home Assistant otomasyonlarına uygundur.',
|
||||
manageArchives: 'Arşivleri yönet',
|
||||
manageArchivesDescription: 'Eski baskıların kaldırılması dahil olmak üzere baskı arşivlerini düzenleyin ve silin. İstatistik katkılarının temizlenmesini içermez. Baskı geçmişini budayan otomasyonlar için uygundur.',
|
||||
manageProjects: 'Projeleri yönet',
|
||||
manageProjectsDescription: 'Projeleri oluşturun, güncelleyin ve silin, ayrıca bunlara arşiv ekleyin. Baskıları projeler halinde düzenleyen otomasyonlar için uygundur.',
|
||||
maintenanceBadge: 'Bakım',
|
||||
archivesBadge: 'Arşivler',
|
||||
projectsBadge: 'Projeler',
|
||||
libraryBadge: 'Kütüphane',
|
||||
inventoryBadge: 'Envanter',
|
||||
cloudAccess: 'Bulut erişimine izin ver',
|
||||
|
||||
@@ -1993,8 +1993,11 @@ export default {
|
||||
manageMaintenanceDescription: '记录已完成的维护、重置计数器、编辑间隔并管理维护类型目录。适用于在不授予更广泛打印机控制权限的情况下记录"我清洁了喷嘴"的 Home Assistant 自动化。',
|
||||
manageArchives: '管理打印存档',
|
||||
manageArchivesDescription: '编辑和删除打印存档,包括移除旧的打印。不包括清除其统计数据贡献。适用于精简打印历史的自动化。',
|
||||
manageProjects: '管理项目',
|
||||
manageProjectsDescription: '创建、更新和删除项目,并向其添加存档。适用于将打印整理到项目中的自动化。',
|
||||
maintenanceBadge: '维护',
|
||||
archivesBadge: '存档',
|
||||
projectsBadge: '项目',
|
||||
libraryBadge: '资料库',
|
||||
inventoryBadge: '库存',
|
||||
cloudAccess: '允许云端访问',
|
||||
|
||||
@@ -1993,8 +1993,11 @@ export default {
|
||||
manageMaintenanceDescription: '記錄已完成的維護、重設計數器、編輯間隔並管理維護類型目錄。適用於在不授予更廣泛印表機控制權限的情況下記錄「我清潔了噴嘴」的 Home Assistant 自動化。',
|
||||
manageArchives: '管理列印封存',
|
||||
manageArchivesDescription: '編輯和刪除列印封存,包括移除舊的列印。不包括清除其統計資料貢獻。適用於精簡列印歷史的自動化。',
|
||||
manageProjects: '管理專案',
|
||||
manageProjectsDescription: '建立、更新和刪除專案,並向其新增封存。適用於將列印整理到專案中的自動化。',
|
||||
maintenanceBadge: '維護',
|
||||
archivesBadge: '封存',
|
||||
projectsBadge: '專案',
|
||||
libraryBadge: '資料庫',
|
||||
inventoryBadge: '庫存',
|
||||
cloudAccess: '允許雲端存取',
|
||||
|
||||
@@ -236,6 +236,7 @@ export function SettingsPage() {
|
||||
can_manage_inventory: true,
|
||||
can_manage_maintenance: true,
|
||||
can_manage_archives: true,
|
||||
can_manage_projects: true,
|
||||
can_access_cloud: false,
|
||||
can_update_energy_cost: false,
|
||||
});
|
||||
@@ -395,7 +396,7 @@ export function SettingsPage() {
|
||||
});
|
||||
|
||||
const createAPIKeyMutation = useMutation({
|
||||
mutationFn: (data: { name: string; can_queue: boolean; can_control_printer: boolean; can_read_status: boolean; can_manage_library: boolean; can_manage_inventory: boolean; can_manage_maintenance: boolean; can_manage_archives: boolean; can_access_cloud: boolean }) =>
|
||||
mutationFn: (data: { name: string; can_queue: boolean; can_control_printer: boolean; can_read_status: boolean; can_manage_library: boolean; can_manage_inventory: boolean; can_manage_maintenance: boolean; can_manage_archives: boolean; can_manage_projects: boolean; can_access_cloud: boolean }) =>
|
||||
api.createAPIKey(data),
|
||||
onSuccess: (data) => {
|
||||
setCreatedAPIKey(data.key || null);
|
||||
@@ -3884,6 +3885,18 @@ export function SettingsPage() {
|
||||
<p className="text-xs text-bambu-gray">{t('settings.manageArchivesDescription')}</p>
|
||||
</div>
|
||||
</label>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newAPIKeyPermissions.can_manage_projects}
|
||||
onChange={(e) => setNewAPIKeyPermissions(prev => ({ ...prev, can_manage_projects: e.target.checked }))}
|
||||
className="w-4 h-4 text-bambu-green rounded border-bambu-dark-tertiary bg-bambu-dark focus:ring-bambu-green"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-white">{t('settings.manageProjects')}</span>
|
||||
<p className="text-xs text-bambu-gray">{t('settings.manageProjectsDescription')}</p>
|
||||
</div>
|
||||
</label>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -3977,6 +3990,9 @@ export function SettingsPage() {
|
||||
{key.can_manage_archives && (
|
||||
<span className="px-1.5 py-0.5 bg-indigo-500/20 text-indigo-400 rounded">{t('settings.archivesBadge')}</span>
|
||||
)}
|
||||
{key.can_manage_projects && (
|
||||
<span className="px-1.5 py-0.5 bg-lime-500/20 text-lime-400 rounded">{t('settings.projectsBadge')}</span>
|
||||
)}
|
||||
{key.can_access_cloud && (
|
||||
<span className="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded">{t('settings.cloudBadge', 'Cloud')}</span>
|
||||
)}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DnnRRVNb.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Bwc9H1Fy.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BxVhuRti.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user