feat(web): redesign config screen with animated particle background and unified auth layout
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

This commit is contained in:
2026-07-13 23:12:16 +02:00
parent 35ff3f37e1
commit 62a337f3cc
2 changed files with 295 additions and 80 deletions

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card'
import * as Tabs from '$lib/components/ui/tabs'
import { Input } from '$lib/components/ui/input'
import { Label } from '$lib/components/ui/label'
import { Button } from '$lib/components/ui/button'
import { Separator } from '$lib/components/ui/separator'
import { Lock, LogIn, Server } from '@lucide/svelte'
import { fetchWithAuth, setConfig, initConfig, getConfig, clearConfig } from '$lib/config'
import { startLogin, logout as oidcLogout, getUser, isOIDCConfigured } from '$lib/oidc'
import ConfigBackground from '$lib/components/ConfigBackground.svelte'
let { onConnected, onCancel }: { onConnected: () => void; onCancel?: () => void } = $props()
@@ -45,7 +46,7 @@
}
saveToDesktop()
onConnected()
} catch (e) {
} catch {
error = 'Could not reach server — check the URL'
} finally {
connecting = false
@@ -58,7 +59,7 @@
try {
wails.Call.ByName('SaveConfig', apiUrl.trim(), token.trim())
} catch {
// ignore — optional desktop-only path
// optional desktop-only path
}
}
@@ -78,84 +79,127 @@
oidcUser = null
oidcConfigured = false
}
// OIDC callback state: user just returned from Authentik
let showOidcContinue = $derived(oidcConfigured && oidcUser)
</script>
<div class="flex h-svh items-center justify-center p-6">
<Card.Root class="w-full max-w-md">
<Card.Header>
<Card.Title>Connect to Oikos</Card.Title>
<Card.Description>Enter the server URL and your access token.</Card.Description>
</Card.Header>
<Card.Content>
<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">Login with Authentik</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="token">
<form class="flex flex-col gap-4" onsubmit={(e) => { e.preventDefault(); connect() }}>
<div class="flex flex-col gap-1.5">
<Label for="server-url">Server URL</Label>
<Input
id="server-url"
type="url"
placeholder="https://oikos.hubris.network (leave blank if same-origin)"
bind:value={apiUrl}
/>
</div>
<div class="flex flex-col gap-1.5">
<Label for="token">Token</Label>
<Input id="token" type="password" placeholder="bearer token" bind:value={token} />
</div>
{#if error}
<p class="text-sm text-destructive">{error}</p>
{/if}
<div class="flex gap-2">
<Button type="submit" disabled={connecting} class="flex-1">
{connecting ? 'Connecting…' : 'Connect'}
</Button>
{#if onCancel}
<Button type="button" variant="outline" onclick={onCancel}>Cancel</Button>
{/if}
</div>
{#if existing.token}
<Button type="button" variant="ghost" size="sm" onclick={disconnect}>
Forget saved connection
</Button>
{/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>
<div class="relative h-svh overflow-hidden bg-background">
<ConfigBackground />
<div class="relative z-10 flex h-full items-center justify-center p-6">
<div class="w-full max-w-[26rem] space-y-8 rounded-2xl border border-white/8 bg-card/60 p-8 shadow-2xl shadow-black/40 backdrop-blur-xl">
<!-- Logo + heading -->
<div class="flex flex-col items-center gap-4">
<svg viewBox="0 0 91 100" class="h-16 w-16 fill-white/90" aria-hidden="true">
<path d="m45.601 1q20.993 0 33.71 15.946 10.799 13.625 10.799 31.287 0 12.414-5.9548 25.131-5.9548 12.717-16.451 19.176-10.395 6.4592-23.213 6.4592-20.892 0-33.205-16.653-10.395-14.029-10.395-31.489 0-12.717 6.2577-25.232 6.3584-12.616 16.653-18.57 10.295-6.0556 21.801-6.0556zm-3.128 6.5605q-5.3492 0-10.799 3.2296-5.3492 3.1287-8.68 11.102-3.3305 7.9735-3.3305 20.488 0 20.185 7.973 34.82 8.0743 14.634 21.195 14.634 9.7896 0 16.149-8.0743 6.3584-8.0743 6.3584-27.755 0-24.627-10.597-38.756-7.1657-9.6888-18.268-9.6888z" />
</svg>
<div class="text-center">
<h1 class="text-2xl font-semibold tracking-tight text-foreground">Connect to Oikos</h1>
<p class="mt-1.5 text-sm text-muted-foreground">Configure your control room connection</p>
</div>
</div>
<!-- Server URL -->
<div class="flex flex-col gap-1.5">
<Label for="server-url" class="text-xs font-medium">Server URL</Label>
<div class="relative">
<Server class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50" />
<Input
id="server-url"
type="url"
placeholder="https://oikos.hubris.network"
class="pl-9"
bind:value={apiUrl}
/>
</div>
</div>
{#if showOidcContinue}
<!-- OIDC authenticated state -->
<div class="flex flex-col items-center gap-3 rounded-xl border border-white/5 bg-background/40 p-5">
<p class="text-sm text-muted-foreground">
Logged in as <span class="font-semibold text-foreground">{oidcUser}</span>
</p>
<div class="flex w-full gap-2">
<Button type="button" variant="default" class="flex-1" onclick={() => onConnected()}>
Continue to Dashboard
</Button>
{#if onCancel}
<Button type="button" variant="outline" onclick={onCancel}>Cancel</Button>
{/if}
</div>
</Tabs.Content>
</Tabs.Root>
</Card.Content>
</Card.Root>
<Button type="button" variant="ghost" size="sm" onclick={logoutOIDC}>
Sign out
</Button>
</div>
{:else}
<div class="flex items-center gap-3">
<Separator decorative class="flex-1" />
<span class="text-[11px] font-medium uppercase tracking-widest text-muted-foreground/60">authenticate with</span>
<Separator decorative class="flex-1" />
</div>
<!-- Auth options: Authentik + token -->
<div class="flex w-full gap-3">
<!-- Authentik column -->
<div class="flex flex-1 flex-col rounded-xl border border-white/5 bg-background/40 p-4">
<p class="mb-3 text-center text-xs text-muted-foreground">Single sign-on</p>
<Button
type="button"
variant="secondary"
disabled={oidcLoggingIn}
onclick={loginWithAuthentik}
class="w-full gap-2"
>
<LogIn class="size-4" />
{oidcLoggingIn ? 'Redirecting…' : 'Login with Authentik'}
</Button>
</div>
<!-- Token column -->
<div class="flex flex-1 flex-col rounded-xl border border-white/5 bg-background/40 p-4">
<p class="mb-3 text-center text-xs text-muted-foreground">Access token</p>
<form
class="flex flex-col gap-2.5"
onsubmit={(e) => { e.preventDefault(); connect() }}
>
<div class="relative">
<Lock class="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground/50" />
<Input
type="password"
placeholder="Bearer token"
class="pl-8 text-sm"
bind:value={token}
/>
</div>
<Button type="submit" disabled={connecting} size="sm" class="w-full gap-1.5">
{connecting ? 'Connecting…' : 'Connect'}
</Button>
</form>
</div>
</div>
{/if}
<!-- Error -->
{#if error}
<p class="rounded-lg bg-destructive/10 px-3 py-2 text-center text-sm text-destructive">{error}</p>
{/if}
<!-- Footer: cancel + forget -->
{#if !showOidcContinue && (onCancel || existing.token)}
<div class="flex items-center justify-center gap-2">
{#if onCancel}
<Button type="button" variant="ghost" size="sm" onclick={onCancel}>Cancel</Button>
{/if}
{#if existing.token}
<Button type="button" variant="ghost" size="sm" onclick={disconnect}>
Forget saved connection
</Button>
{/if}
</div>
{/if}
</div>
</div>
</div>