SPA navigates to 127.0.0.1:18901/oidc/start, passing ret URL. Go opens browser, waits for callback, saves token, returns HTML with <meta refresh> back to Wails app with ?desktop=1&token=TOKEN. main.ts extracts token from URL on reload.
203 lines
7.2 KiB
Svelte
203 lines
7.2 KiB
Svelte
<script lang="ts">
|
|
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()
|
|
|
|
const existing = getConfig()
|
|
let apiUrl = $state(existing.apiUrl ?? '')
|
|
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() {
|
|
error = ''
|
|
if (!token.trim()) {
|
|
error = 'Token is required'
|
|
return
|
|
}
|
|
connecting = true
|
|
setConfig({ apiUrl: apiUrl.trim(), token: token.trim() })
|
|
initConfig({ apiUrl: apiUrl.trim(), token: token.trim() })
|
|
try {
|
|
const res = await fetchWithAuth('/api/v1/dashboard/summary')
|
|
if (!res.ok) {
|
|
error = res.status === 401 ? 'Invalid token' : `Server responded ${res.status}`
|
|
return
|
|
}
|
|
saveToDesktop()
|
|
onConnected()
|
|
} catch {
|
|
error = 'Could not reach server — check the URL'
|
|
} finally {
|
|
connecting = false
|
|
}
|
|
}
|
|
|
|
function saveToDesktop() {
|
|
const wails = (window as any).wails
|
|
if (!wails?.Call?.ByName) return
|
|
try {
|
|
wails.Call.ByName('SaveConfig', apiUrl.trim(), token.trim())
|
|
} catch {
|
|
// optional desktop-only path
|
|
}
|
|
}
|
|
|
|
async function loginWithAuthentik() {
|
|
error = ''
|
|
oidcLoggingIn = true
|
|
setConfig({ apiUrl: apiUrl.trim(), token: '' })
|
|
initConfig({ apiUrl: apiUrl.trim(), token: '' })
|
|
|
|
try {
|
|
await startLogin()
|
|
} catch (e: any) {
|
|
error = e.message || 'OIDC login failed'
|
|
oidcLoggingIn = false
|
|
}
|
|
}
|
|
|
|
function logoutOIDC() {
|
|
oidcLogout()
|
|
oidcUser = null
|
|
oidcConfigured = false
|
|
}
|
|
|
|
// OIDC callback state: user just returned from Authentik
|
|
let showOidcContinue = $derived(oidcConfigured && oidcUser)
|
|
</script>
|
|
|
|
<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>
|
|
<Button type="button" variant="ghost" size="sm" onclick={logoutOIDC}>
|
|
Sign out
|
|
</Button>
|
|
</div>
|
|
{:else}
|
|
<!-- Auth options: Authentik + token -->
|
|
<div class="flex w-full flex-col gap-3">
|
|
<!-- Authentik row -->
|
|
<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 class="flex items-center gap-3">
|
|
<Separator decorative class="flex-1" />
|
|
<span class="text-[10px] font-medium uppercase tracking-widest text-muted-foreground/40">or use</span>
|
|
<Separator decorative class="flex-1" />
|
|
</div>
|
|
|
|
<!-- Token row -->
|
|
<form
|
|
class="flex flex-col gap-2.5"
|
|
onsubmit={(e) => { e.preventDefault(); connect() }}
|
|
>
|
|
<div class="relative">
|
|
<Lock class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50" />
|
|
<Input
|
|
type="password"
|
|
placeholder="Bearer token"
|
|
class="pl-9"
|
|
bind:value={token}
|
|
/>
|
|
</div>
|
|
<Button type="submit" disabled={connecting} class="w-full">
|
|
{connecting ? 'Connecting…' : 'Connect'}
|
|
</Button>
|
|
</form>
|
|
</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>
|