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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user