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,
|
api,
|
||||||
ApiError,
|
ApiError,
|
||||||
type AppDomain,
|
type AppDomain,
|
||||||
|
type AppRole,
|
||||||
type ExecutionLog,
|
type ExecutionLog,
|
||||||
type Route,
|
type Route,
|
||||||
type RouteInput,
|
type RouteInput,
|
||||||
type Script,
|
type Script,
|
||||||
type VersionInfo
|
type VersionInfo
|
||||||
} from '$lib/api';
|
} from '$lib/api';
|
||||||
|
import { currentUser } from '$lib/auth';
|
||||||
|
import { canAdminApp, canWriteApp } from '$lib/capabilities';
|
||||||
import { logLevelColor, statusColor } from '$lib/styles';
|
import { logLevelColor, statusColor } from '$lib/styles';
|
||||||
import {
|
import {
|
||||||
checkHostAgainstClaims,
|
checkHostAgainstClaims,
|
||||||
@@ -47,6 +50,11 @@
|
|||||||
|
|
||||||
let appSlug = $state<string | null>(null);
|
let appSlug = $state<string | null>(null);
|
||||||
let appDomains = $state<AppDomain[]>([]);
|
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() {
|
async function loadScript() {
|
||||||
scriptLoading = true;
|
scriptLoading = true;
|
||||||
@@ -58,15 +66,16 @@
|
|||||||
editableDescription = script.description ?? '';
|
editableDescription = script.description ?? '';
|
||||||
editableTimeout = script.timeout_seconds;
|
editableTimeout = script.timeout_seconds;
|
||||||
editableSandbox = { ...(script.sandbox ?? {}) };
|
editableSandbox = { ...(script.sandbox ?? {}) };
|
||||||
// Resolve the owning app's slug for the breadcrumb and its
|
// Resolve the owning app for the breadcrumb (slug),
|
||||||
// domain claims for the route form's suggestions + live
|
// route-form host suggestions (domain claims), and UI
|
||||||
// validation. Both are non-fatal — the page works without
|
// shadowing (my_role on this app). All non-fatal — the
|
||||||
// them.
|
// page renders without them, just with reduced fidelity.
|
||||||
const appId = script.app_id;
|
const appId = script.app_id;
|
||||||
void api.apps
|
void api.apps
|
||||||
.get(appId)
|
.get(appId)
|
||||||
.then((a) => {
|
.then((a) => {
|
||||||
appSlug = a.slug;
|
appSlug = a.slug;
|
||||||
|
appMyRole = a.my_role ?? null;
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
void api.domains
|
void api.domains
|
||||||
@@ -386,6 +395,15 @@
|
|||||||
void loadRoutes();
|
void loadRoutes();
|
||||||
void loadLogs();
|
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>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@@ -410,9 +428,11 @@
|
|||||||
v{script.version} · timeout {script.timeout_seconds}s · {script.description ?? 'no description'}
|
v{script.version} · timeout {script.timeout_seconds}s · {script.description ?? 'no description'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{#if canAdmin}
|
||||||
<button type="button" class="danger" onclick={remove} disabled={deleting}>
|
<button type="button" class="danger" onclick={remove} disabled={deleting}>
|
||||||
{deleting ? 'Deleting…' : 'Delete'}
|
{deleting ? 'Deleting…' : 'Delete'}
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
@@ -423,7 +443,9 @@
|
|||||||
<span class="badge-count">{routes.length}</span>
|
<span class="badge-count">{routes.length}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
{#if canAdmin}
|
||||||
<button class:active={tab === 'settings'} onclick={() => (tab = 'settings')}>Settings</button>
|
<button class:active={tab === 'settings'} onclick={() => (tab = 'settings')}>Settings</button>
|
||||||
|
{/if}
|
||||||
<button class:active={tab === 'executions'} onclick={() => (tab = 'executions')}>
|
<button class:active={tab === 'executions'} onclick={() => (tab = 'executions')}>
|
||||||
Executions
|
Executions
|
||||||
</button>
|
</button>
|
||||||
@@ -435,17 +457,25 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<header class="editor-header">
|
<header class="editor-header">
|
||||||
<h2>Source</h2>
|
<h2>Source</h2>
|
||||||
|
{#if canWrite}
|
||||||
<button type="button" class="ghost small" onclick={formatRhaiSource}>
|
<button type="button" class="ghost small" onclick={formatRhaiSource}>
|
||||||
Format
|
Format
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
<CodeEditor bind:value={editableSource} language="rhai" minHeight="22rem" />
|
<CodeEditor
|
||||||
|
bind:value={editableSource}
|
||||||
|
language="rhai"
|
||||||
|
minHeight="22rem"
|
||||||
|
readOnly={!canWrite}
|
||||||
|
/>
|
||||||
{#if rhaiFormatError}
|
{#if rhaiFormatError}
|
||||||
<div class="error inline">{rhaiFormatError}</div>
|
<div class="error inline">{rhaiFormatError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if saveSourceError}
|
{#if saveSourceError}
|
||||||
<div class="error inline">{saveSourceError}</div>
|
<div class="error inline">{saveSourceError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if canWrite}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -455,6 +485,7 @@
|
|||||||
{savingSource ? 'Saving…' : 'Save'}
|
{savingSource ? 'Saving…' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
@@ -510,12 +541,14 @@
|
|||||||
<section class="card wide">
|
<section class="card wide">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<h2>Routes</h2>
|
<h2>Routes</h2>
|
||||||
|
{#if canWrite}
|
||||||
<button type="button" onclick={() => (showAddRoute = !showAddRoute)}>
|
<button type="button" onclick={() => (showAddRoute = !showAddRoute)}>
|
||||||
{showAddRoute ? 'Cancel' : '+ Add route'}
|
{showAddRoute ? 'Cancel' : '+ Add route'}
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if showAddRoute}
|
{#if showAddRoute && canWrite}
|
||||||
<form class="route-form" onsubmit={submitRoute}>
|
<form class="route-form" onsubmit={submitRoute}>
|
||||||
<label class="full">
|
<label class="full">
|
||||||
<span>Path</span>
|
<span>Path</span>
|
||||||
@@ -626,9 +659,11 @@
|
|||||||
: r.host}
|
: r.host}
|
||||||
</span>
|
</span>
|
||||||
<span class="path">{r.path}</span>
|
<span class="path">{r.path}</span>
|
||||||
|
{#if canWrite}
|
||||||
<button type="button" class="link danger" onclick={() => removeRoute(r.id)}>
|
<button type="button" class="link danger" onclick={() => removeRoute(r.id)}>
|
||||||
remove
|
remove
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if info}
|
{#if info}
|
||||||
<div class="route-url muted">→ {fullUrlForRoute(r)}</div>
|
<div class="route-url muted">→ {fullUrlForRoute(r)}</div>
|
||||||
@@ -670,7 +705,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ===================================================== SETTINGS ===== -->
|
<!-- ===================================================== SETTINGS ===== -->
|
||||||
{:else if tab === 'settings'}
|
{:else if tab === 'settings' && canAdmin}
|
||||||
<section class="card wide">
|
<section class="card wide">
|
||||||
<h2>General</h2>
|
<h2>General</h2>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
Reference in New Issue
Block a user