mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-19 05:47:48 +00:00
fix(webui): address auth-gate review findings
Explicit refocus. Add a watch(() => auth.error, ...) that calls the same focusKeyInput() helper onMounted already used. Previously, refocus-after-failure only worked through an undocumented chain (auth.submit() optimistically flips isAuthenticated, App.vue unmounts AuthGate, a 401 calls auth.reject(), isAuthenticated flips back, AuthGate remounts, onMounted refires). That chain still exists and still drives the shake retrigger; a comment above the Input now says so explicitly, so a future change to optimistic auth does not break either behavior silently. Security disclosure order. Move the three-item checklist above the form, so the claims sit in the reading path before the key field instead of below the submit button. Add a visually hidden `<h2 id="authgate-security-heading">` and tie the `<ul>` to it via aria-labelledby, giving screen-reader users a label the list did not have before. Wording and text-xs sizing are unchanged. Invalid-state wiring. Add :aria-invalid and :aria-describedby to the Input, pointing at a new id="authgate-error" on the Alert, closing the loop between the input and its own error message. Typography hierarchy. CardTitle grows to text-lg font-semibold; the "LLM Gateway" wordmark drops to a muted-foreground text-xs eyebrow, so the title now reads as the dominant line instead of losing to the wordmark's former text-sm font-semibold. Drop text-base from the Input's class list — Input.vue already sets text-base (overridden by its own md:text-sm at >=768px); the local copy did nothing but restate the default. h-10/pr-10 stay.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { faCheck, faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck, faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -31,10 +31,22 @@ function submit(): void {
|
|||||||
void dashboard.refresh()
|
void dashboard.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
function focusKeyInput(): void {
|
||||||
// Autofocus the key field — this is the only control on the gate screen,
|
|
||||||
// so a keyboard/screen-reader user should land straight in it.
|
|
||||||
void nextTick(() => keyInputEl.value?.$el?.focus())
|
void nextTick(() => keyInputEl.value?.$el?.focus())
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(focusKeyInput)
|
||||||
|
|
||||||
|
// Explicit refocus on a failed key: auth.submit() optimistically sets
|
||||||
|
// apiKey before the server has validated it, which flips isAuthenticated
|
||||||
|
// and makes App.vue unmount AuthGate; the ensuing 401 calls auth.reject()
|
||||||
|
// (apiKey='', error=message), isAuthenticated flips back, and AuthGate
|
||||||
|
// remounts — re-running onMounted's focus above as a side effect. That
|
||||||
|
// chain is real and correct today, but silent: if optimistic auth is ever
|
||||||
|
// removed, refocus would stop working with no signal. Watching auth.error
|
||||||
|
// directly makes refocus explicit and no longer dependent on the remount.
|
||||||
|
watch(() => auth.error, (err) => {
|
||||||
|
if (err) focusKeyInput()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -46,14 +58,40 @@ onMounted(() => {
|
|||||||
<FontAwesomeIcon :icon="faKey" class="size-5" aria-hidden="true" />
|
<FontAwesomeIcon :icon="faKey" class="size-5" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center gap-1">
|
<div class="flex flex-col items-center gap-1">
|
||||||
<span class="text-sm font-semibold text-foreground">LLM Gateway</span>
|
<span class="text-xs font-medium text-muted-foreground">LLM Gateway</span>
|
||||||
<CardTitle>Admin key required</CardTitle>
|
<CardTitle class="text-lg font-semibold">Admin key required</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>Enter an admin API key to continue.</CardDescription>
|
<CardDescription>Enter an admin API key to continue.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="flex flex-col gap-4">
|
<CardContent class="flex flex-col gap-4">
|
||||||
|
<h2 id="authgate-security-heading" class="sr-only">
|
||||||
|
How this key is handled
|
||||||
|
</h2>
|
||||||
|
<ul aria-labelledby="authgate-security-heading" class="flex flex-col gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<li class="flex items-start gap-1.5">
|
||||||
|
<FontAwesomeIcon :icon="faCheck" class="mt-0.5 size-3 shrink-0" aria-hidden="true" />
|
||||||
|
<span>Kept only in this tab's session storage</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-1.5">
|
||||||
|
<FontAwesomeIcon :icon="faCheck" class="mt-0.5 size-3 shrink-0" aria-hidden="true" />
|
||||||
|
<span>Never written to disk</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-1.5">
|
||||||
|
<FontAwesomeIcon :icon="faCheck" class="mt-0.5 size-3 shrink-0" aria-hidden="true" />
|
||||||
|
<span>Sent only to this page's own /admin/api/* requests</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<form class="flex flex-col gap-3" @submit.prevent="submit">
|
<form class="flex flex-col gap-3" @submit.prevent="submit">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
<!--
|
||||||
|
authgate-shake retriggers via the AuthGate remount on a
|
||||||
|
failed submit (see the refocus watcher in <script> for the
|
||||||
|
exact chain) — a v-if unmount/remount recreates this
|
||||||
|
element, which replays the CSS animation. If that remount
|
||||||
|
chain ever changes, the shake stops retriggering silently
|
||||||
|
along with it (left as-is per review — the mechanism itself
|
||||||
|
is verified correct today).
|
||||||
|
-->
|
||||||
<Input
|
<Input
|
||||||
ref="keyInputEl"
|
ref="keyInputEl"
|
||||||
v-model="keyInput"
|
v-model="keyInput"
|
||||||
@@ -61,7 +99,9 @@ onMounted(() => {
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="API key"
|
placeholder="API key"
|
||||||
aria-label="Admin API key"
|
aria-label="Admin API key"
|
||||||
class="h-10 pr-10 text-base"
|
:aria-invalid="!!auth.error"
|
||||||
|
:aria-describedby="auth.error ? 'authgate-error' : undefined"
|
||||||
|
class="h-10 pr-10"
|
||||||
:class="{ 'authgate-shake': auth.error }"
|
:class="{ 'authgate-shake': auth.error }"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -79,24 +119,10 @@ onMounted(() => {
|
|||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
<Alert v-if="auth.error" variant="destructive">
|
<Alert v-if="auth.error" id="authgate-error" variant="destructive">
|
||||||
<AlertTitle>Authentication failed</AlertTitle>
|
<AlertTitle>Authentication failed</AlertTitle>
|
||||||
<AlertDescription>{{ auth.error }}</AlertDescription>
|
<AlertDescription>{{ auth.error }}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<ul class="flex flex-col gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<li class="flex items-start gap-1.5">
|
|
||||||
<FontAwesomeIcon :icon="faCheck" class="mt-0.5 size-3 shrink-0" aria-hidden="true" />
|
|
||||||
<span>Kept only in this tab's session storage</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-start gap-1.5">
|
|
||||||
<FontAwesomeIcon :icon="faCheck" class="mt-0.5 size-3 shrink-0" aria-hidden="true" />
|
|
||||||
<span>Never written to disk</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-start gap-1.5">
|
|
||||||
<FontAwesomeIcon :icon="faCheck" class="mt-0.5 size-3 shrink-0" aria-hidden="true" />
|
|
||||||
<span>Sent only to this page's own /admin/api/* requests</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user