diff --git a/dashboard/src/routes/scripts/[id]/+page.svelte b/dashboard/src/routes/scripts/[id]/+page.svelte index 640fcd5..75f5897 100644 --- a/dashboard/src/routes/scripts/[id]/+page.svelte +++ b/dashboard/src/routes/scripts/[id]/+page.svelte @@ -6,12 +6,15 @@ api, ApiError, type AppDomain, + type AppRole, type ExecutionLog, type Route, type RouteInput, type Script, type VersionInfo } from '$lib/api'; + import { currentUser } from '$lib/auth'; + import { canAdminApp, canWriteApp } from '$lib/capabilities'; import { logLevelColor, statusColor } from '$lib/styles'; import { checkHostAgainstClaims, @@ -47,6 +50,11 @@ let appSlug = $state(null); let appDomains = $state([]); + let appMyRole = $state(null); + + const me = $derived($currentUser); + const canWrite = $derived(canWriteApp(me, appMyRole)); + const canAdmin = $derived(canAdminApp(me, appMyRole)); async function loadScript() { scriptLoading = true; @@ -58,15 +66,16 @@ editableDescription = script.description ?? ''; editableTimeout = script.timeout_seconds; editableSandbox = { ...(script.sandbox ?? {}) }; - // Resolve the owning app's slug for the breadcrumb and its - // domain claims for the route form's suggestions + live - // validation. Both are non-fatal — the page works without - // them. + // Resolve the owning app for the breadcrumb (slug), + // route-form host suggestions (domain claims), and UI + // shadowing (my_role on this app). All non-fatal — the + // page renders without them, just with reduced fidelity. const appId = script.app_id; void api.apps .get(appId) .then((a) => { appSlug = a.slug; + appMyRole = a.my_role ?? null; }) .catch(() => {}); void api.domains @@ -386,6 +395,15 @@ void loadRoutes(); void loadLogs(); }); + + // Defense-in-depth: anyone non-admin who lands on the Settings + // tab via a stale link gets bounced back to Edit. The tab button + // itself is also hidden. + $effect(() => { + if (!canAdmin && tab === 'settings') { + tab = 'edit'; + } + });
@@ -410,9 +428,11 @@ v{script.version} · timeout {script.timeout_seconds}s · {script.description ?? 'no description'}

- + {#if canAdmin} + + {/if}