Files
traefik-llmgateway/webui/src/components/AuthGate.vue
T
lukaszraczylo b98416d1e9 fix(webui): version display, CSP guards, config traps, bucket-aligned stacking
Review fix round (4 Important + 2 folded):

1. Version display: OverviewView.vue was missing the muted version line
   the old vanilla-JS page rendered (types/api.ts already declared
   overview.version) - added.

2. CSP invariant now guarded twice: webui/generate.mjs's new
   assertNoInlineAssets fails the build if dist/index.html contains an
   inline <script> (no src= attribute) or any <style> tag - the exact
   condition the tightened admin CSP (script-src/style-src 'self', no
   'unsafe-inline') depends on. admin_test.go's new
   TestAdminIndexHTML_NoInlineAssets re-asserts the identical invariant
   server-side, against the decoded adminIndexHTML admin.go actually
   serves, so a regression is caught even if admin_assets_gen.go were
   regenerated with a modified copy of generate.mjs.

3. webui/components.json's iconLibrary:"lucide"/font:"inter" fields
   cannot be fixed in place - checked shadcn-vue's own schema
   (PRESET_ICON_LIBRARIES/PRESET_FONTS in the installed package): no
   "none"/fontawesome/system choice exists for either, and JSON (unlike
   the tsconfig.*.json files touched in the previous commit) has no
   comment syntax to warn inline. Documented instead: webui/README.md
   gained a loud warning section naming the exact regression (already
   hit twice this branch - a re-added Google Fonts import, a re-added
   lucide icon import) and the two-step fix to apply after any future
   shadcn-vue add/init call.

4. Real-CSP verification: the browser pass earlier in this branch ran
   under vite preview, which sends no CSP header at all. New
   integration/integration_test.go: TestAdminAssetCSP fetches GET
   /admin against the real Traefik+Yaegi pipeline, extracts the asset
   URL from its <script src="..."> tag, fetches that asset, and asserts
   both responses carry the exact adminCSP string byte-for-byte.
   admin.go's serveAdminAsset now also sets Content-Security-Policy
   (previously JSON/page routes only) so this holds for a direct
   navigation to an asset URL, not just as a same-origin sub-resource of
   GET /admin; admin_test.go's TestAdminAsset_Serving updated to match.

5. (folded) UsageChart.vue stacked the "tokens" tab's two series
   (tokin/tokout - two separate HTTP requests, not one atomic call) by
   positional index. A window rollover landing between the two requests
   could leave one series one bucket ahead, mis-stacking that refresh.
   Now aligns both series by their bucket key (a sorted union of every
   active series' buckets - the fixed-width, zero-padded bucket strings
   sort chronologically by construction), with a missing bucket in one
   series reading as 0 rather than shifting the whole series.

6. (folded) AuthGate.vue now triggers an immediate dashboard.refresh()
   on successful key submit, instead of waiting up to 5s for the next
   poll tick - the dashboard no longer sits blank right after auth.

Rebuilt via make admin-ui; admin_assets_gen.go regenerated.
2026-08-21 09:43:24 +01:00

60 lines
2.1 KiB
Vue

<script setup lang="ts">
import { faKey } from '@fortawesome/free-solid-svg-icons'
import { ref } from 'vue'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { useAuthStore } from '@/stores/auth'
import { useDashboardStore } from '@/stores/dashboard'
const auth = useAuthStore()
const dashboard = useDashboardStore()
const keyInput = ref('')
function submit(): void {
const value = keyInput.value
keyInput.value = ''
auth.submit(value)
// Fetch immediately rather than waiting for the next 5s poll tick
// (dashboard.startPolling's own interval, already running from App.vue's
// onMounted) — otherwise a freshly authenticated dashboard can sit blank
// for up to 5s after key entry.
void dashboard.refresh()
}
</script>
<template>
<div class="mx-auto flex min-h-[70vh] max-w-md flex-col justify-center px-4">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<FontAwesomeIcon :icon="faKey" class="size-4 text-primary" />
Admin key required
</CardTitle>
<CardDescription>
Enter an admin API key. The browser keeps it only in this tab's session storage: it is
never written to disk and is sent only to this page's own /admin/api/* requests.
</CardDescription>
</CardHeader>
<CardContent class="flex flex-col gap-4">
<form class="flex flex-col gap-3" @submit.prevent="submit">
<Input
v-model="keyInput"
type="password"
autocomplete="off"
placeholder="API key"
aria-label="Admin API key"
/>
<Button type="submit" class="w-full">Continue</Button>
</form>
<Alert v-if="auth.error" variant="destructive">
<AlertTitle>Authentication failed</AlertTitle>
<AlertDescription>{{ auth.error }}</AlertDescription>
</Alert>
</CardContent>
</Card>
</div>
</template>