Replace Zitadel's built-in login v1 with a fully custom SvelteKit-based login experience using Zitadel Session API v2. Keeps the existing OIDC authorization code flow (Auth.js handles token exchange) while providing branded login, signup, password reset, and TOTP pages. - Enable Login V2 in docker-compose, assign IAM_LOGIN_CLIENT role in setup script - Add server-only Zitadel API client ($lib/server/zitadel.ts) with session, user, and auth-request management functions - Create reusable auth UI components (AuthCard, FormField, FormError, LoadingButton) - Rewrite login page with email/password form and TOTP second factor support - Add signup page with auto-login after registration - Add password reset flow (request + verify pages) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
84 lines
2.6 KiB
Svelte
84 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import { enhance } from '$app/forms';
|
|
import AuthCard from '$lib/components/AuthCard.svelte';
|
|
import FormField from '$lib/components/FormField.svelte';
|
|
import FormError from '$lib/components/FormError.svelte';
|
|
import LoadingButton from '$lib/components/LoadingButton.svelte';
|
|
|
|
let { data, form } = $props();
|
|
|
|
let loading = $state(false);
|
|
let success = $derived(form?.success === true);
|
|
let authRequest = $derived(form?.authRequest ?? data.authRequest);
|
|
let loginHref = $derived(authRequest ? `/login?authRequest=${authRequest}` : '/login');
|
|
</script>
|
|
|
|
<AuthCard
|
|
title="Reset your password"
|
|
subtitle="Enter your email and we'll send you a reset link"
|
|
>
|
|
{#if success}
|
|
<div class="flex items-start gap-3 px-4 py-3 bg-emerald-500/10 border border-emerald-500/30 rounded-lg">
|
|
<svg class="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
|
|
</svg>
|
|
<div>
|
|
<p class="text-sm text-emerald-400 font-medium">Check your email</p>
|
|
<p class="text-sm text-slate-400 mt-1">If an account exists with that email, we've sent a password reset link.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<a
|
|
href={loginHref}
|
|
class="w-full flex items-center justify-center px-4 py-3 bg-white/5 border border-white/10 text-white rounded-lg font-medium transition-all duration-200 hover:bg-white/10"
|
|
>
|
|
Back to sign in
|
|
</a>
|
|
</div>
|
|
{:else}
|
|
<FormError message={form?.error} />
|
|
|
|
<form
|
|
method="POST"
|
|
action="?/requestReset"
|
|
use:enhance={() => {
|
|
loading = true;
|
|
return async ({ update }) => {
|
|
loading = false;
|
|
await update();
|
|
};
|
|
}}
|
|
class="space-y-5 {form?.error ? 'mt-4' : ''}"
|
|
>
|
|
<input type="hidden" name="authRequest" value={data.authRequest} />
|
|
|
|
<FormField
|
|
name="email"
|
|
type="email"
|
|
label="Email"
|
|
placeholder="you@example.com"
|
|
autocomplete="email"
|
|
required
|
|
/>
|
|
|
|
<LoadingButton {loading}>
|
|
Send reset link
|
|
</LoadingButton>
|
|
</form>
|
|
|
|
<p class="text-center text-sm text-slate-500 mt-6">
|
|
Remember your password?
|
|
<a
|
|
href={loginHref}
|
|
class="text-blue-400 hover:text-blue-300 transition-colors"
|
|
>
|
|
Sign in
|
|
</a>
|
|
</p>
|
|
{/if}
|
|
|
|
{#snippet footer()}
|
|
<a href="/" class="hover:text-slate-300 transition-colors">← Back to home</a>
|
|
{/snippet}
|
|
</AuthCard>
|