mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 23:21:58 +02:00
Add inventory:view_assignments permission so admins can grant users
visibility into AMS spool assignments on printer cards without exposing the full Inventory page (#635). The list_assignments endpoint now requires this new permission instead of inventory:read. All default groups (Administrators, Operators, Viewers) include it for backward compatibility. Closes #634
This commit is contained in:
@@ -4,6 +4,12 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
|
||||
## [0.2.2b3] - Unreleased
|
||||
|
||||
### New Features
|
||||
- **Malaysian Ringgit Currency** ([#634](https://github.com/maziggy/bambuddy/issues/634)) — Added MYR (RM) to the list of supported currencies for filament cost tracking. Requested by @cynogen127.
|
||||
|
||||
### Improved
|
||||
- **Separate Permission for AMS Spool Assignments** ([#635](https://github.com/maziggy/bambuddy/issues/635)) — Added a new `inventory:view_assignments` permission that controls whether spool-to-AMS-slot assignment data is visible on the Printers page. Previously, viewing spool assignments on printer cards required `inventory:read`, which also exposed the full Inventory page in the sidebar. Admins can now grant `inventory:view_assignments` without `inventory:read` so users can see what's loaded in the AMS without accessing the full spool inventory. All default groups (Administrators, Operators, Viewers) include the new permission automatically. Also fixed multi-word permission labels in the group editor (e.g. "Update_Own" → "Update Own"). Reported by @Minebuddy.
|
||||
|
||||
### Fixed
|
||||
- **Dispatch Toast Stuck After Second Print** — The print dispatch progress toast ("Starting prints…") stayed visible forever after the second print dispatch in a session. The dedup guard (`lastDispatchSummaryRef`) that prevents duplicate completion toasts was never reset between batches, so every single-printer dispatch produced the same summary key (`"first-complete:1:0"`). The first print completed normally, but subsequent completions matched the stale ref and skipped creating the done toast — leaving the progress toast stuck in "Processing" state with no way to dismiss except a page reload. Now resets the dedup guard whenever the dispatch toast is dismissed (auto-dismiss timeout, cleanup events) and when a new batch starts.
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ Perfect for remote print farms, traveling makers, or accessing your home printer
|
||||
|
||||
### 🔒 Optional Authentication
|
||||
- Enable/disable authentication any time
|
||||
- Group-based permissions (50+ granular permissions)
|
||||
- Group-based permissions (80+ granular permissions)
|
||||
- Default groups: Administrators, Operators, Viewers
|
||||
- JWT tokens with secure password hashing
|
||||
- Comprehensive API protection (200+ endpoints secured)
|
||||
|
||||
@@ -35,7 +35,7 @@ def _permission_label(perm: Permission) -> str:
|
||||
if len(parts) == 2:
|
||||
resource, action = parts
|
||||
resource = resource.replace("_", " ").title()
|
||||
action = action.title()
|
||||
action = action.replace("_", " ").title()
|
||||
return f"{action} {resource}"
|
||||
return perm.value
|
||||
|
||||
|
||||
@@ -636,7 +636,7 @@ async def replace_k_profiles(
|
||||
async def list_assignments(
|
||||
printer_id: int | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_VIEW_ASSIGNMENTS),
|
||||
):
|
||||
"""List spool assignments, optionally filtered by printer."""
|
||||
from backend.app.services.printer_manager import printer_manager
|
||||
|
||||
@@ -68,6 +68,7 @@ class Permission(StrEnum):
|
||||
INVENTORY_CREATE = "inventory:create"
|
||||
INVENTORY_UPDATE = "inventory:update"
|
||||
INVENTORY_DELETE = "inventory:delete"
|
||||
INVENTORY_VIEW_ASSIGNMENTS = "inventory:view_assignments" # View spool-to-AMS assignments on printer cards
|
||||
|
||||
# Smart Plugs
|
||||
SMART_PLUGS_READ = "smart_plugs:read"
|
||||
@@ -214,6 +215,7 @@ PERMISSION_CATEGORIES = {
|
||||
Permission.INVENTORY_CREATE,
|
||||
Permission.INVENTORY_UPDATE,
|
||||
Permission.INVENTORY_DELETE,
|
||||
Permission.INVENTORY_VIEW_ASSIGNMENTS,
|
||||
],
|
||||
"Smart Plugs": [
|
||||
Permission.SMART_PLUGS_READ,
|
||||
@@ -355,6 +357,7 @@ DEFAULT_GROUPS = {
|
||||
Permission.INVENTORY_CREATE.value,
|
||||
Permission.INVENTORY_UPDATE.value,
|
||||
Permission.INVENTORY_DELETE.value,
|
||||
Permission.INVENTORY_VIEW_ASSIGNMENTS.value,
|
||||
# Smart Plugs - full access
|
||||
Permission.SMART_PLUGS_READ.value,
|
||||
Permission.SMART_PLUGS_CREATE.value,
|
||||
@@ -411,6 +414,7 @@ DEFAULT_GROUPS = {
|
||||
Permission.PROJECTS_READ.value,
|
||||
Permission.FILAMENTS_READ.value,
|
||||
Permission.INVENTORY_READ.value,
|
||||
Permission.INVENTORY_VIEW_ASSIGNMENTS.value,
|
||||
Permission.SMART_PLUGS_READ.value,
|
||||
Permission.CAMERA_VIEW.value,
|
||||
Permission.MAINTENANCE_READ.value,
|
||||
|
||||
@@ -74,3 +74,41 @@ class TestPermissionCategoriesCompleteness:
|
||||
for perm in perms:
|
||||
assert perm not in seen, f"{perm} in both '{seen[perm]}' and '{cat_name}'"
|
||||
seen[perm] = cat_name
|
||||
|
||||
|
||||
class TestInventoryViewAssignmentsPermission:
|
||||
"""Test the INVENTORY_VIEW_ASSIGNMENTS permission."""
|
||||
|
||||
def test_view_assignments_permission_exists(self):
|
||||
"""inventory:view_assignments permission should exist in the enum."""
|
||||
assert hasattr(Permission, "INVENTORY_VIEW_ASSIGNMENTS")
|
||||
assert Permission.INVENTORY_VIEW_ASSIGNMENTS == "inventory:view_assignments"
|
||||
|
||||
def test_view_assignments_in_all_permissions(self):
|
||||
"""inventory:view_assignments should be in ALL_PERMISSIONS list."""
|
||||
assert "inventory:view_assignments" in ALL_PERMISSIONS
|
||||
|
||||
def test_view_assignments_in_inventory_category(self):
|
||||
"""inventory:view_assignments should be in the Inventory permission category."""
|
||||
inventory_perms = PERMISSION_CATEGORIES["Inventory"]
|
||||
assert Permission.INVENTORY_VIEW_ASSIGNMENTS in inventory_perms
|
||||
|
||||
def test_view_assignments_separate_from_read(self):
|
||||
"""view_assignments and read should be distinct permissions."""
|
||||
assert Permission.INVENTORY_VIEW_ASSIGNMENTS != Permission.INVENTORY_READ
|
||||
assert Permission.INVENTORY_VIEW_ASSIGNMENTS.value != Permission.INVENTORY_READ.value
|
||||
|
||||
def test_operators_have_view_assignments(self):
|
||||
"""Operators group should include inventory:view_assignments."""
|
||||
operators = DEFAULT_GROUPS["Operators"]
|
||||
assert "inventory:view_assignments" in operators["permissions"]
|
||||
|
||||
def test_viewers_have_view_assignments(self):
|
||||
"""Viewers group should include inventory:view_assignments."""
|
||||
viewers = DEFAULT_GROUPS["Viewers"]
|
||||
assert "inventory:view_assignments" in viewers["permissions"]
|
||||
|
||||
def test_administrators_have_view_assignments(self):
|
||||
"""Administrators should have all permissions including view_assignments."""
|
||||
admins = DEFAULT_GROUPS["Administrators"]
|
||||
assert "inventory:view_assignments" in admins["permissions"]
|
||||
|
||||
@@ -22,6 +22,10 @@ describe('getCurrencySymbol', () => {
|
||||
expect(getCurrencySymbol('HKD')).toBe('HK$');
|
||||
});
|
||||
|
||||
it('returns RM for MYR', () => {
|
||||
expect(getCurrencySymbol('MYR')).toBe('RM');
|
||||
});
|
||||
|
||||
it('returns the code itself for unknown currencies', () => {
|
||||
expect(getCurrencySymbol('XYZ')).toBe('XYZ');
|
||||
});
|
||||
@@ -37,7 +41,11 @@ describe('SUPPORTED_CURRENCIES', () => {
|
||||
expect(SUPPORTED_CURRENCIES.find((c) => c.code === 'INR')).toBeDefined();
|
||||
});
|
||||
|
||||
it('has 26 entries', () => {
|
||||
expect(SUPPORTED_CURRENCIES).toHaveLength(26);
|
||||
it('contains MYR', () => {
|
||||
expect(SUPPORTED_CURRENCIES.find((c) => c.code === 'MYR')).toBeDefined();
|
||||
});
|
||||
|
||||
it('has 27 entries', () => {
|
||||
expect(SUPPORTED_CURRENCIES).toHaveLength(27);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2023,7 +2023,7 @@ export type Permission =
|
||||
| 'library:update_own' | 'library:update_all' | 'library:delete_own' | 'library:delete_all'
|
||||
| 'projects:read' | 'projects:create' | 'projects:update' | 'projects:delete'
|
||||
| 'filaments:read' | 'filaments:create' | 'filaments:update' | 'filaments:delete'
|
||||
| 'inventory:read' | 'inventory:create' | 'inventory:update' | 'inventory:delete'
|
||||
| 'inventory:read' | 'inventory:create' | 'inventory:update' | 'inventory:delete' | 'inventory:view_assignments'
|
||||
| 'smart_plugs:read' | 'smart_plugs:create' | 'smart_plugs:update' | 'smart_plugs:delete' | 'smart_plugs:control'
|
||||
| 'camera:view'
|
||||
| 'maintenance:read' | 'maintenance:create' | 'maintenance:update' | 'maintenance:delete'
|
||||
|
||||
@@ -1337,7 +1337,7 @@ export function AmsNameHoverCard({
|
||||
});
|
||||
}
|
||||
}, [isVisible, amsLabels, ams.id]);
|
||||
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => setIsVisible(true), 80);
|
||||
@@ -5368,6 +5368,7 @@ export function PrintersPage() {
|
||||
const { data: spoolAssignments } = useQuery({
|
||||
queryKey: ['spool-assignments'],
|
||||
queryFn: () => api.getAssignments(),
|
||||
enabled: hasPermission('inventory:view_assignments'),
|
||||
staleTime: 30 * 1000,
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -23,7 +23,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DCHZw0ab.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BpZJje_K.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DfcIVNpM.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user