fix(stage-2): audit dashboard TS alignment + login UX

Closes 4 audit findings:

- Route TS interface now carries dispatch_mode (sync|async). Backend's
  shared::route::Route has had this since 0012_routes_dispatch_mode but
  the TS client silently always created sync routes and the list
  display dropped the field. Add a Dispatch select to the new-route
  form and an "ASYNC" badge in the route list.

- api.files.downloadUrl pointed to a never-registered backend endpoint.
  The dashboard's live Download button was hitting 405. Add the GET
  handler (AppFilesRead + FilesRepo::head + FilesRepo::get, Content-
  Disposition: inline) at the same path that delete already used.

- F-T-003: adminRequest's 401 handler called goto(login) without a
  recursion cap. If the login endpoint itself returned 401, the wrapper
  looped until browser nav limits. Track consecutive 401s within a 10s
  window and hard-reload to /login?reason=auth-loop after the third,
  showing the user an explanatory banner. Any 2xx resets the counter.

- Login flow now honors a ?returnTo= query parameter. adminRequest and
  the root layout both append the current location when redirecting to
  /login; the login page validates the value is a same-origin admin
  path (no open-redirect) and goto's there after successful sign-in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-10 21:09:53 +02:00
parent 3c9816daf3
commit f466b2a15e
5 changed files with 166 additions and 10 deletions

View File

@@ -209,6 +209,7 @@
// canonical display form for an unrestricted host.
let newRouteHost = $state('*');
let newRouteMethod = $state('');
let newRouteDispatchMode = $state<'sync' | 'async'>('sync');
let pathKindAutoUpdate = $state(true);
let creatingRoute = $state(false);
let createRouteError = $state<string | null>(null);
@@ -255,13 +256,15 @@
host: parsedHost.host,
path_kind: newRoutePathKind,
path: newRoutePath.trim(),
method: newRouteMethod.trim() || null
method: newRouteMethod.trim() || null,
dispatch_mode: newRouteDispatchMode
};
await api.routes.create(id, input);
showAddRoute = false;
newRoutePath = '/';
newRouteHost = '*';
newRouteMethod = '';
newRouteDispatchMode = 'sync';
pathKindAutoUpdate = true;
await loadRoutes();
} catch (e) {
@@ -619,6 +622,13 @@
<option value="PATCH">PATCH</option>
</select>
</label>
<label>
<span>Dispatch</span>
<select bind:value={newRouteDispatchMode}>
<option value="sync">sync (wait for response)</option>
<option value="async">async (202 + run later)</option>
</select>
</label>
</div>
<label class="full">
<span class="host-label">
@@ -695,6 +705,9 @@
: r.host}
</span>
<span class="path">{r.path}</span>
{#if r.dispatch_mode === 'async'}
<span class="dispatch dispatch-async" title="Dispatch mode">async</span>
{/if}
{#if canWrite}
<button type="button" class="link danger" onclick={() => requestRemoveRoute(r)}>
remove
@@ -1258,6 +1271,18 @@
color: #e2e8f0;
flex: 1;
}
.route-row .dispatch {
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
padding: 0.1rem 0.4rem;
border-radius: 0.25rem;
letter-spacing: 0.03em;
}
.dispatch-async {
background: #422006;
color: #fcd34d;
}
.route-url {
font-family:
ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;