feat(dashboard): shadow script-detail surfaces by role
Captures my_role off the existing parent-app fetch (no extra HTTP call) and uses canWriteApp / canAdminApp to hide: header Delete, Edit Save + Format, Routing +Add route + per-row remove, and the Settings tab. CodeEditor renders read-only for viewers. An effect bounces a stale Settings tab back to Edit for non-admins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null>(null);
|
||||
let appDomains = $state<AppDomain[]>([]);
|
||||
let appMyRole = $state<AppRole | null>(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';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section>
|
||||
@@ -410,9 +428,11 @@
|
||||
v{script.version} · timeout {script.timeout_seconds}s · {script.description ?? 'no description'}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="danger" onclick={remove} disabled={deleting}>
|
||||
{deleting ? 'Deleting…' : 'Delete'}
|
||||
</button>
|
||||
{#if canAdmin}
|
||||
<button type="button" class="danger" onclick={remove} disabled={deleting}>
|
||||
{deleting ? 'Deleting…' : 'Delete'}
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<nav class="tabs">
|
||||
@@ -423,7 +443,9 @@
|
||||
<span class="badge-count">{routes.length}</span>
|
||||
{/if}
|
||||
</button>
|
||||
<button class:active={tab === 'settings'} onclick={() => (tab = 'settings')}>Settings</button>
|
||||
{#if canAdmin}
|
||||
<button class:active={tab === 'settings'} onclick={() => (tab = 'settings')}>Settings</button>
|
||||
{/if}
|
||||
<button class:active={tab === 'executions'} onclick={() => (tab = 'executions')}>
|
||||
Executions
|
||||
</button>
|
||||
@@ -435,26 +457,35 @@
|
||||
<section class="card">
|
||||
<header class="editor-header">
|
||||
<h2>Source</h2>
|
||||
<button type="button" class="ghost small" onclick={formatRhaiSource}>
|
||||
Format
|
||||
</button>
|
||||
{#if canWrite}
|
||||
<button type="button" class="ghost small" onclick={formatRhaiSource}>
|
||||
Format
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
<CodeEditor bind:value={editableSource} language="rhai" minHeight="22rem" />
|
||||
<CodeEditor
|
||||
bind:value={editableSource}
|
||||
language="rhai"
|
||||
minHeight="22rem"
|
||||
readOnly={!canWrite}
|
||||
/>
|
||||
{#if rhaiFormatError}
|
||||
<div class="error inline">{rhaiFormatError}</div>
|
||||
{/if}
|
||||
{#if saveSourceError}
|
||||
<div class="error inline">{saveSourceError}</div>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<button
|
||||
type="button"
|
||||
onclick={saveSource}
|
||||
disabled={savingSource || editableSource === script.source}
|
||||
>
|
||||
{savingSource ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
{#if canWrite}
|
||||
<div class="actions">
|
||||
<button
|
||||
type="button"
|
||||
onclick={saveSource}
|
||||
disabled={savingSource || editableSource === script.source}
|
||||
>
|
||||
{savingSource ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
@@ -510,12 +541,14 @@
|
||||
<section class="card wide">
|
||||
<header class="card-header">
|
||||
<h2>Routes</h2>
|
||||
<button type="button" onclick={() => (showAddRoute = !showAddRoute)}>
|
||||
{showAddRoute ? 'Cancel' : '+ Add route'}
|
||||
</button>
|
||||
{#if canWrite}
|
||||
<button type="button" onclick={() => (showAddRoute = !showAddRoute)}>
|
||||
{showAddRoute ? 'Cancel' : '+ Add route'}
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if showAddRoute}
|
||||
{#if showAddRoute && canWrite}
|
||||
<form class="route-form" onsubmit={submitRoute}>
|
||||
<label class="full">
|
||||
<span>Path</span>
|
||||
@@ -626,9 +659,11 @@
|
||||
: r.host}
|
||||
</span>
|
||||
<span class="path">{r.path}</span>
|
||||
<button type="button" class="link danger" onclick={() => removeRoute(r.id)}>
|
||||
remove
|
||||
</button>
|
||||
{#if canWrite}
|
||||
<button type="button" class="link danger" onclick={() => removeRoute(r.id)}>
|
||||
remove
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if info}
|
||||
<div class="route-url muted">→ {fullUrlForRoute(r)}</div>
|
||||
@@ -670,7 +705,7 @@
|
||||
</section>
|
||||
|
||||
<!-- ===================================================== SETTINGS ===== -->
|
||||
{:else if tab === 'settings'}
|
||||
{:else if tab === 'settings' && canAdmin}
|
||||
<section class="card wide">
|
||||
<h2>General</h2>
|
||||
<label>
|
||||
|
||||
Reference in New Issue
Block a user