Browse Source

Fix power-off option not gated by printers:control permission (#500)

The "Power off printer when done" checkbox in the print modal and the
auto power off tri-state toggle in the bulk edit modal were accessible
to all users regardless of permissions. Users without printers:control
can now no longer enable auto power off — controls are disabled and
visually dimmed.
maziggy 3 months ago
parent
commit
04ac6bd36e

+ 3 - 0
CHANGELOG.md

@@ -4,6 +4,9 @@ All notable changes to Bambuddy will be documented in this file.
 
 ## [0.2.1b4] - Unreleased
 
+### Fixed
+- **"Power Off Printer" Option Not Gated by Control Permission** ([#500](https://github.com/maziggy/bambuddy/issues/500)) — The "Power off printer when done" checkbox in the print modal and the auto power off toggle in the bulk edit modal were accessible to all users regardless of permissions. Users without the `printers:control` permission can now no longer enable auto power off — the checkbox and tri-state toggle are disabled and visually dimmed.
+
 ## [0.2.1b3] - 2026-02-23
 
 ### Fixed

+ 4 - 2
frontend/src/components/PrintModal/ScheduleOptions.tsx

@@ -23,6 +23,7 @@ export function ScheduleOptionsPanel({
   onChange,
   dateFormat = 'system',
   timeFormat = 'system',
+  canControlPrinter = true,
 }: ScheduleOptionsProps) {
   const [dateValue, setDateValue] = useState('');
   const [timeValue, setTimeValue] = useState('');
@@ -237,9 +238,10 @@ export function ScheduleOptionsPanel({
           id="autoOffAfter"
           checked={options.autoOffAfter}
           onChange={(e) => onChange({ ...options, autoOffAfter: e.target.checked })}
-          className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
+          disabled={!canControlPrinter}
+          className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green disabled:opacity-50"
         />
-        <label htmlFor="autoOffAfter" className="text-sm text-bambu-gray flex items-center gap-1">
+        <label htmlFor="autoOffAfter" className={`text-sm flex items-center gap-1 ${canControlPrinter ? 'text-bambu-gray' : 'text-bambu-gray/50'}`}>
           <Power className="w-3.5 h-3.5" />
           Power off printer when done
         </label>

+ 3 - 0
frontend/src/components/PrintModal/index.tsx

@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import type { PrintQueueItemCreate, PrintQueueItemUpdate } from '../../api/client';
 import { api } from '../../api/client';
+import { useAuth } from '../../contexts/AuthContext';
 import { useToast } from '../../contexts/ToastContext';
 import { useFilamentMapping } from '../../hooks/useFilamentMapping';
 import { useMultiPrinterFilamentMapping, type PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping';
@@ -48,6 +49,7 @@ export function PrintModal({
   const { t } = useTranslation();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
+  const { hasPermission } = useAuth();
 
   // Determine if we're printing a library file
   const isLibraryFile = !!libraryFileId && !archiveId;
@@ -775,6 +777,7 @@ export function PrintModal({
                 onChange={setScheduleOptions}
                 dateFormat={settings?.date_format || 'system'}
                 timeFormat={settings?.time_format || 'system'}
+                canControlPrinter={hasPermission('printers:control')}
               />
             )}
 

+ 2 - 0
frontend/src/components/PrintModal/types.ts

@@ -194,4 +194,6 @@ export interface ScheduleOptionsProps {
   dateFormat?: 'system' | 'us' | 'eu' | 'iso';
   /** Time format setting from user preferences */
   timeFormat?: 'system' | '12h' | '24h';
+  /** Whether the user has permission to control printers (for auto power off) */
+  canControlPrinter?: boolean;
 }

+ 13 - 5
frontend/src/pages/QueuePage.tsx

@@ -111,6 +111,7 @@ function BulkEditModal({
   onSave,
   onClose,
   isSaving,
+  canControlPrinter,
   t,
 }: {
   selectedCount: number;
@@ -118,6 +119,7 @@ function BulkEditModal({
   onSave: (data: Partial<PrintQueueBulkUpdate>) => void;
   onClose: () => void;
   isSaving: boolean;
+  canControlPrinter: boolean;
   t: (key: string, options?: Record<string, unknown>) => string;
 }) {
   const [printerId, setPrinterId] = useState<number | null | 'unchanged'>('unchanged');
@@ -193,7 +195,7 @@ function BulkEditModal({
             <label className="block text-sm font-medium text-white mb-2">{t('queue.bulkEdit.queueOptions')}</label>
             <div className="space-y-2">
               <TriStateToggle label={t('queue.bulkEdit.staged')} value={manualStart} onChange={setManualStart} t={t} />
-              <TriStateToggle label={t('queue.bulkEdit.autoPowerOff')} value={autoOffAfter} onChange={setAutoOffAfter} t={t} />
+              <TriStateToggle label={t('queue.bulkEdit.autoPowerOff')} value={autoOffAfter} onChange={setAutoOffAfter} disabled={!canControlPrinter} t={t} />
               <TriStateToggle label={t('queue.bulkEdit.requirePrevious')} value={requirePreviousSuccess} onChange={setRequirePreviousSuccess} t={t} />
             </div>
           </div>
@@ -231,32 +233,37 @@ function TriStateToggle({
   label,
   value,
   onChange,
+  disabled,
   t,
 }: {
   label: string;
   value: boolean | 'unchanged';
   onChange: (val: boolean | 'unchanged') => void;
+  disabled?: boolean;
   t: (key: string) => string;
 }) {
   return (
-    <div className="flex items-center justify-between py-1">
+    <div className={`flex items-center justify-between py-1 ${disabled ? 'opacity-50' : ''}`}>
       <span className="text-sm text-bambu-gray">{label}</span>
       <div className="flex items-center gap-1 bg-bambu-dark rounded-lg p-0.5">
         <button
           onClick={() => onChange('unchanged')}
-          className={`px-2 py-1 text-xs rounded ${value === 'unchanged' ? 'bg-bambu-dark-tertiary text-white' : 'text-bambu-gray hover:text-white'}`}
+          disabled={disabled}
+          className={`px-2 py-1 text-xs rounded ${value === 'unchanged' ? 'bg-bambu-dark-tertiary text-white' : 'text-bambu-gray hover:text-white'} disabled:cursor-not-allowed`}
         >
         </button>
         <button
           onClick={() => onChange(false)}
-          className={`px-2 py-1 text-xs rounded ${value === false ? 'bg-red-500/20 text-red-400' : 'text-bambu-gray hover:text-white'}`}
+          disabled={disabled}
+          className={`px-2 py-1 text-xs rounded ${value === false ? 'bg-red-500/20 text-red-400' : 'text-bambu-gray hover:text-white'} disabled:cursor-not-allowed`}
         >
           {t('common.off')}
         </button>
         <button
           onClick={() => onChange(true)}
-          className={`px-2 py-1 text-xs rounded ${value === true ? 'bg-bambu-green/20 text-bambu-green' : 'text-bambu-gray hover:text-white'}`}
+          disabled={disabled}
+          className={`px-2 py-1 text-xs rounded ${value === true ? 'bg-bambu-green/20 text-bambu-green' : 'text-bambu-gray hover:text-white'} disabled:cursor-not-allowed`}
         >
           {t('common.on')}
         </button>
@@ -1431,6 +1438,7 @@ export function QueuePage() {
           }}
           onClose={() => setShowBulkEditModal(false)}
           isSaving={bulkUpdateMutation.isPending}
+          canControlPrinter={hasPermission('printers:control')}
           t={t}
         />
       )}

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BIJADXVl.js


+ 1 - 1
static/index.html

@@ -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-DHMy9Wxo.js"></script>
+    <script type="module" crossorigin src="/assets/index-BIJADXVl.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-1Ts9jjQl.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff