mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-27 20:29:10 +00:00
fix(auto-heal): gate panel write controls on admin role (#1245)
The Auto-Heal panel rendered its add-policy form, the per-policy enable toggle, and the per-policy delete button without any admin guard, but POST/PATCH/DELETE on /api/auto-heal/policies enforce requireAdmin + requirePaid. Paid non-admin users could see the controls and would hit a 403 on click. Same drift class as the Schedule task gate already moved in the prior sidebar parity pass. Gate the add-new-policy section, the enable toggle, and the delete button on the admin role. Reading existing policies and viewing history stay available to every paid user, matching the backend GET routes which are requirePaid only. Docs updated to call out the admin requirement on Auto-Heal write actions and to add a troubleshooting accordion for non-admin operators who see the panel without the configuration controls.
This commit is contained in:
@@ -85,7 +85,7 @@ Right-click any stack (or open the kebab that appears on hover) for its context
|
||||
- **Destructive**: **Delete**.
|
||||
|
||||
<Note>
|
||||
**Auto-Heal** requires a **Skipper** or **Admiral** license. **Schedule task** requires a **Skipper** or **Admiral** license and an **admin** role.
|
||||
**Auto-Heal** requires a **Skipper** or **Admiral** license; configuring policies (add, toggle, delete) also requires an **admin** role. **Schedule task** requires a **Skipper** or **Admiral** license and an **admin** role.
|
||||
</Note>
|
||||
|
||||
<Frame>
|
||||
@@ -146,4 +146,7 @@ The footer surfaces the most recent stack lifecycle event on the node. Each tick
|
||||
<Accordion title="Schedule task is missing from the right-click menu">
|
||||
Scheduling requires a **Skipper** or **Admiral** license and an **admin** role. Ask an admin on this node to schedule the task for you, or sign in with an admin account.
|
||||
</Accordion>
|
||||
<Accordion title="Auto-Heal panel opens but Add Policy and the toggles are missing">
|
||||
Reading existing Auto-Heal policies only needs a **Skipper** or **Admiral** license, so the panel still opens for non-admin operators on paid tiers. Adding, enabling, disabling, or deleting policies also requires the **admin** role. Sign in with an admin account, or ask an admin on this node to make the change.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -482,6 +482,7 @@ function AlertsTab({ stackName }: { stackName: string }) {
|
||||
}
|
||||
|
||||
function AutoHealTab({ stackName, open }: { stackName: string; open: boolean }) {
|
||||
const { isAdmin } = useAuth();
|
||||
const [policies, setPolicies] = useState<AutoHealPolicy[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -617,12 +618,14 @@ function AutoHealTab({ stackName, open }: { stackName: string; open: boolean })
|
||||
onToggle={handleToggle}
|
||||
deleting={deleting}
|
||||
saving={saving}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SheetSection>
|
||||
|
||||
{isAdmin && (
|
||||
<SheetSection title="Add new policy">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
@@ -700,6 +703,7 @@ function AutoHealTab({ stackName, open }: { stackName: string; open: boolean })
|
||||
</Button>
|
||||
</div>
|
||||
</SheetSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -710,9 +714,10 @@ interface PolicyRowProps {
|
||||
onToggle: (id: number, enabled: boolean) => void;
|
||||
deleting: boolean;
|
||||
saving: boolean;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
function PolicyRow({ policy, onDelete, onToggle, deleting, saving }: PolicyRowProps) {
|
||||
function PolicyRow({ policy, onDelete, onToggle, deleting, saving, isAdmin }: PolicyRowProps) {
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const [history, setHistory] = useState<AutoHealHistoryEntry[]>([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
@@ -760,12 +765,14 @@ function PolicyRow({ policy, onDelete, onToggle, deleting, saving }: PolicyRowPr
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<TogglePill
|
||||
checked={policy.enabled === 1}
|
||||
onChange={(checked) => policy.id != null && onToggle(policy.id, checked)}
|
||||
disabled={saving}
|
||||
aria-label={`Toggle policy for ${policy.service_name ?? 'all services'}`}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<TogglePill
|
||||
checked={policy.enabled === 1}
|
||||
onChange={(checked) => policy.id != null && onToggle(policy.id, checked)}
|
||||
disabled={saving}
|
||||
aria-label={`Toggle policy for ${policy.service_name ?? 'all services'}`}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -782,16 +789,18 @@ function PolicyRow({ policy, onDelete, onToggle, deleting, saving }: PolicyRowPr
|
||||
<ChevronDown className="h-4 w-4" strokeWidth={1.5} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive/60 hover:bg-destructive hover:text-destructive-foreground"
|
||||
onClick={() => policy.id != null && onDelete(policy.id)}
|
||||
disabled={deleting}
|
||||
aria-label="Delete policy"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" strokeWidth={1.5} />
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive/60 hover:bg-destructive hover:text-destructive-foreground"
|
||||
onClick={() => policy.id != null && onDelete(policy.id)}
|
||||
disabled={deleting}
|
||||
aria-label="Delete policy"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" strokeWidth={1.5} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user