9856c04b75
ci / rust (push) Has been cancelled
Single-user, LAN-reachable-but-gated. The web server is a backend-for-frontend:
- Login: POST /_auth/login {password} checks PUNKTFUNK_UI_PASSWORD (constant-time) and
sets a SEALED session cookie (h3 useSession / AES-GCM). server/middleware/auth.ts gates
every request — pages 302 → /login, /api → 401 — and FAILS CLOSED (503) when
PUNKTFUNK_UI_PASSWORD is unset, so a misconfigured LAN-exposed server admits no one.
- The management API stays loopback-only + token (never LAN-exposed). The proxy
(server/routes/api/[...].ts) injects PUNKTFUNK_MGMT_TOKEN server-side and drops the
browser's cookie before forwarding — the token never reaches the browser, which only
holds the session cookie.
Nitro doesn't auto-scan a server/ dir, so the Nitro plugin gets an explicit scanDirs to
pick up middleware + routes. Client: removed the localStorage token (server injects it);
the fetcher bounces to /login on 401; new /login page (bare, no shell); Settings drops the
token field and gains a Sign-out button; en/de strings.
Validated live end to end: unauth /→302, /api→401; wrong pw→401; right pw→200+cookie;
authed /api/v1/status→200 (proxied, mgmt token injected — the host required it); logout→
session cleared→401. tsc + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
1.6 KiB
TypeScript
55 lines
1.6 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
|
import { LogOut } from 'lucide-react'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { m } from '@/paraglide/messages'
|
|
import { useLocale, changeLocale, locales, type Locale } from '@/lib/i18n'
|
|
|
|
export const Route = createFileRoute('/settings')({ component: SettingsPage })
|
|
|
|
function SettingsPage() {
|
|
const current = useLocale()
|
|
|
|
const onLogout = async () => {
|
|
await fetch('/_auth/logout', { method: 'POST' })
|
|
window.location.href = '/login'
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-2xl font-semibold">{m.settings_title()}</h1>
|
|
|
|
<Card className="max-w-lg">
|
|
<CardHeader>
|
|
<CardTitle>{m.settings_language()}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="flex gap-2">
|
|
{locales.map((l: Locale) => (
|
|
<Button
|
|
key={l}
|
|
variant={l === current ? 'default' : 'outline'}
|
|
size="sm"
|
|
className="uppercase"
|
|
onClick={() => changeLocale(l)}
|
|
>
|
|
{l}
|
|
</Button>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="max-w-lg">
|
|
<CardHeader>
|
|
<CardTitle>{m.nav_settings()}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button variant="outline" onClick={onLogout}>
|
|
<LogOut className="size-4" />
|
|
{m.action_logout()}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|