oidc: authenticate SPA users via Authentik

- Add OIDC proxy endpoints (GET config, POST token) to API server
- Implement PKCE Authorization Code flow in SPA
- Enable Authentik login tab in Config page
- Handle callback + auto-refresh + session restore
- Add restart: unless-stopped to all persistent services
- Configure OIDC issuer + client_id in docker-compose
This commit is contained in:
2026-07-13 22:17:31 +02:00
parent 4c4afc4783
commit f6a699469d
7 changed files with 501 additions and 14 deletions

View File

@@ -5,6 +5,7 @@
import { Label } from '$lib/components/ui/label'
import { Button } from '$lib/components/ui/button'
import { fetchWithAuth, setConfig, initConfig, getConfig, clearConfig } from '$lib/config'
import { startLogin, logout as oidcLogout, getUser, isOIDCConfigured } from '$lib/oidc'
let { onConnected, onCancel }: { onConnected: () => void; onCancel?: () => void } = $props()
@@ -13,12 +14,18 @@
let token = $state(existing.token ?? '')
let connecting = $state(false)
let error = $state('')
let oidcLoggingIn = $state(false)
let oidcUser = $state(getUser())
let oidcConfigured = $state(isOIDCConfigured())
function disconnect() {
clearConfig()
oidcLogout()
apiUrl = ''
token = ''
error = ''
oidcUser = null
oidcConfigured = false
}
async function connect() {
@@ -43,6 +50,23 @@
connecting = false
}
}
async function loginWithAuthentik() {
error = ''
oidcLoggingIn = true
try {
await startLogin()
} catch (e: any) {
error = e.message || 'OIDC login failed'
oidcLoggingIn = false
}
}
function logoutOIDC() {
oidcLogout()
oidcUser = null
oidcConfigured = false
}
</script>
<div class="flex h-svh items-center justify-center p-6">
@@ -52,10 +76,10 @@
<Card.Description>Enter the server URL and your access token.</Card.Description>
</Card.Header>
<Card.Content>
<Tabs.Root value="token">
<Tabs.Root value={oidcConfigured ? 'oidc' : 'token'}>
<Tabs.List class="mb-4 grid w-full grid-cols-2">
<Tabs.Trigger value="token">Token</Tabs.Trigger>
<Tabs.Trigger value="oidc" disabled>Login with Authentik (coming soon)</Tabs.Trigger>
<Tabs.Trigger value="oidc">Login with Authentik</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="token">
<form class="flex flex-col gap-4" onsubmit={(e) => { e.preventDefault(); connect() }}>
@@ -90,6 +114,36 @@
{/if}
</form>
</Tabs.Content>
<Tabs.Content value="oidc">
<div class="flex flex-col gap-4">
{#if oidcConfigured && oidcUser}
<p class="text-sm text-muted-foreground">
Logged in as <span class="font-medium text-foreground">{oidcUser}</span>
</p>
<Button type="button" variant="default" onclick={() => onConnected()}>
Continue to Dashboard
</Button>
<Button type="button" variant="ghost" size="sm" onclick={logoutOIDC}>
Log out
</Button>
{:else}
<p class="text-sm text-muted-foreground">
Sign in with your Authentik account to access the control room.
</p>
{#if error}
<p class="text-sm text-destructive">{error}</p>
{/if}
<Button type="button" disabled={oidcLoggingIn} onclick={loginWithAuthentik}>
{oidcLoggingIn ? 'Redirecting to Authentik…' : 'Login with Authentik'}
</Button>
{/if}
{#if existing.token}
<Button type="button" variant="ghost" size="sm" onclick={disconnect}>
Forget saved connection
</Button>
{/if}
</div>
</Tabs.Content>
</Tabs.Root>
</Card.Content>
</Card.Root>