diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bb390d1..274ec72a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,9 @@ All notable changes to Bambuddy will be documented in this file. - **MakerWorld import/resolve/status fail under API-key auth even when the owner has a Bambu Cloud login (#1777, reported by @Mx772)** — The reporter (working on a browser extension that drives Bambuddy via `X-API-Key`) noticed that `POST /api/v1/makerworld/import` and `POST /api/v1/makerworld/resolve` returned `{"detail":"Downloading files from MakerWorld requires a Bambu Cloud login"}` even when the key's owning user had a valid stored Bambu Cloud session, and the same imports succeeded from the web UI. Root cause is exactly the shape the reporter traced: `require_permission_if_auth_enabled` in `backend/app/core/auth.py:1414` deliberately returns `current_user=None` for API-keyed callers — the comment at line 1408 makes this explicit and points at `cloud.py` for the resolver. The MakerWorld routes never got that resolver wired in, so `_build_service(db, None)` → `get_stored_token(db, None)` → no token → the "requires Bambu Cloud login" branch fires regardless of what the owning account has set up. Same shape #1182 fixed for cloud slicer presets, and the canonical fix for non-`/cloud/*` routes is already in the codebase as `resolve_api_key_cloud_owner` (cloud.py:128-160) — used by `slicer_presets.py:491` and `library.py:3871`. The MakerWorld routes were missing the wire-up. **Fix:** Three routes get the extra `api_key_cloud_owner: User | None = Depends(resolve_api_key_cloud_owner)` parameter — `get_status`, `resolve_url`, `import_instance` — and each resolves `cloud_token_user = current_user or api_key_cloud_owner` before calling `get_stored_token` / `_build_service`. `import_instance` additionally uses `cloud_token_user.id` for the `owner_id` argument to `save_3mf_bytes_to_library` (which translates to `LibraryFile.created_by_id`), so library rows imported via API key are now attributed to the key's owner instead of staying NULL. `/recent-imports` is unchanged — it only uses `current_user` as a permission gate (`_ = current_user`) and never touches the cloud token. The fix preserves fail-closed semantics for keys *without* the `can_access_cloud` flag: `resolve_api_key_cloud_owner` already fences on `api_key.user_id is not None and api_key.can_access_cloud` (cloud.py:158), so a key with only the per-route scope (`can_read_status` / `can_manage_library`) still surfaces the "requires Bambu Cloud login" error path — no new auth gap. **Two scope fields the API key needs:** the per-route scope (`MAKERWORLD_VIEW` → `can_read_status`, `MAKERWORLD_IMPORT` → `can_manage_library` per `_APIKEY_SCOPE_BY_PERMISSION` in `core/auth.py`) AND the orthogonal `can_access_cloud` flag (separate column on the `api_keys` table). The fix doesn't change that surface — it just stops dropping valid `can_access_cloud=True` keys on the floor. **Tests:** 6 new cases in `backend/tests/integration/test_makerworld_apikey_auth.py` pinning the full surface — API key with `can_access_cloud=True` + owner-has-token → `/status` reports `has_cloud_token=True`, `/resolve` builds the service with the owner User (asserted on the `_build_service` mock's call args), `/import` succeeds end-to-end and the resulting `LibraryFile.created_by_id` matches the API-key owner; API key with `can_access_cloud=False` → status still reports `has_cloud_token=False` (no widening) and import-row's `created_by_id` stays NULL; JWT-authenticated parity check confirms the existing user-session flow is unchanged by the added `Depends`. 6/6 new tests green; full backend suite (6157 tests) still green; ruff clean. No frontend change, no DB migration, no new permission, no new dependency. The reporter's browser extension and any other API-keyed Home Assistant / automation integration unblocks immediately on next deploy. - **PrintModal printer picker no longer offers a printer between dispatch-accept and PRINT_START (reported off-list by a corporate user running multi-operator farm shifts)** — Operator picks a printer in the reprint modal, hits Send, Bambuddy accepts the dispatch and begins FTP upload + sending the print command. The printer hasn't reported `gcode_state=RUNNING` yet — it's still IDLE on its own MQTT status. A second operator opening the modal during this window sees the same printer as available and submits a second job. The backend correctly rejects the second submit with HTTP 409 (`background_dispatch._dispatch` rejects when `_queued_jobs` or `_active_jobs` already holds the printer_id), so no double-print is possible, but the operator only finds out after they click Send — wasted minutes per attempt on a busy floor. **Root cause:** `PrinterSelector.tsx::isPrinterBusy` consulted only `PrinterStatus.state` against `AVAILABLE_STATES = {IDLE, FINISH, FAILED}`. PRINT_START is the only signal that flips the printer out of IDLE, and there's a real wall-clock window (upload time + print command + firmware ack) between dispatch acceptance and that flip. The dispatch-queue state — already broadcast as a WebSocket `background_dispatch` push including `dispatched_jobs[].printer_id` and `active_jobs[].printer_id` — was being consumed by `ToastContext` for the progress overlay but never read by the picker. **Fix:** new `frontend/src/hooks/useDispatchedPrinterIds.ts` exposes `Set` of printer_ids with a queued or active dispatch, populated from the same `background-dispatch` window event the ToastContext listens for. Module-level singleton + `useSyncExternalStore` so every `PrinterSelector` instance sees the same snapshot and a modal opened mid-batch picks up the latest state without a refetch. Reference-stable snapshot (size + membership check) keeps `useSyncExternalStore`'s Object.is comparison from re-rendering on every WS push that doesn't change the set. `PrinterSelector.tsx::isPrinterBusy` ORs the set into the existing connected/state check — printer disabled the instant dispatch is accepted, re-enabled when the dispatch finishes (or fails) and disappears from the next state payload. `getPrinterStateLabel` returns `"Dispatching..."` for the badge so operators see the in-flight state instead of a misleading "Idle" on a now-disabled card. Hardcoded English label is consistent with the existing labels in that function (`"Idle"`, `"Printing"`, `"Paused"` are all hardcoded, no i18n key). **What this is NOT:** a backend change (the reservation Mike asked about already exists at `background_dispatch.py:283-290`); a behaviour change for `add-to-queue` / `edit-queue-item` modes (those don't set `disableBusy=true`, so the busy-OR remains dormant for the card click handler — the badge label still flips, which is informative); a guarantee against the WS-not-yet-connected race (a fresh page load that opens the modal before the WS initial-state push lands still sees an empty set for ~1 frame; same race as today, much shorter window). **Tests:** 8 new cases in `useDispatchedPrinterIds.test.ts` pin the contract — empty initial set, picks up `dispatched_jobs` printer_ids, picks up `active_jobs` printer_ids, unions both lists, clears when subsequent event reports zero jobs, ignores non-numeric `printer_id` (defensive against payload drift), reference-stable snapshot when content doesn't change, shared state across hook instances. Existing 84 PrintModal + PrinterSelector cases still green — the new code path is dormant until a `background-dispatch` window event fires, which existing tests don't trigger. `npm run build` clean, ESLint clean. +### Security +- **Bumped two frontend dev-tooling dependencies with denial-of-service advisories (GHSA-3jxr-9vmj-r5cp, GHSA-52cp-r559-cp3m)** — `brace-expansion` and `js-yaml`, both pulled in transitively by `eslint` (via `minimatch` and `@eslint/eslintrc`), were flagged by `npm audit`. They are build/lint-time tooling only and are not part of the shipped app, so no running Bambuddy install was ever exposed. `npm audit fix` couldn't move eslint to the patched versions on its own, so they're pinned to the fixed releases through the existing `overrides` block in `frontend/package.json` (`brace-expansion ^5.0.7`, `js-yaml ^4.3.0`). `npm audit` now reports zero vulnerabilities and eslint still runs clean. + ## [0.2.4.9] - 2026-07-07 ### Added diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2e30e19fb..bc7c6c1c2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3615,11 +3615,10 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -5399,9 +5398,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { diff --git a/frontend/package.json b/frontend/package.json index ebafaffda..920f62e74 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,9 @@ "three": "^0.181.2" }, "overrides": { - "minimatch": "^10.2.1" + "minimatch": "^10.2.1", + "brace-expansion": "^5.0.7", + "js-yaml": "^4.3.0" }, "devDependencies": { "@eslint/js": "^9.39.1",