Files
website/src/lib/components/AuthForm.svelte
Stan Runge 54924b39f0
Some checks failed
Test and deploy staging / staging (push) Failing after 43s
feat: upgrade to SvelteKit 3 and adapter-bun
Run the sveltekit-3 migration and finish its manual tasks:

- Move kit config into the sveltekit() Vite plugin; drop svelte.config.js
- Replace $lib with #lib subpath imports
- Declare env vars in src/env.ts and import them from $app/env/{private,public}
- Consolidate param matchers into src/params.ts
- Use error(status, message), Response.json, refreshAll and goto reset/shallow
- Fall back to a full page load when goto rejects a login return path

Switch from adapter-node to adapter-bun: the build script runs Vite under
Bun (the adapter requires it) and the image starts with `bun ./build`.
Runtime env names (PORT, BODY_SIZE_LIMIT, ADDRESS_HEADER, ...) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:17:30 +00:00

164 lines
4.9 KiB
Svelte

<script lang="ts">
import { authClient } from '#lib/auth-client.js';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { Button } from '#lib/components/ui/button/index.js';
import * as Card from '#lib/components/ui/card/index.js';
import { Input } from '#lib/components/ui/input/index.js';
import { Label } from '#lib/components/ui/label/index.js';
import { Separator } from '#lib/components/ui/separator/index.js';
let {
mode,
providers,
returnTo,
oauthError
}: {
mode: 'login' | 'register';
providers: { google: boolean; osu: boolean };
returnTo: string;
oauthError: boolean;
} = $props();
let name = $state('');
let email = $state('');
let password = $state('');
let busy = $state(false);
let ready = $state(false);
onMount(() => {
ready = true;
});
let message = $state('');
async function submit(event: SubmitEvent) {
event.preventDefault();
const destination = `${resolve('/')}${returnTo.slice(1)}`;
busy = true;
message = '';
try {
const result =
mode === 'register'
? await authClient.signUp.email({ name: name.trim(), email, password })
: await authClient.signIn.email({ email, password });
if (result.error) {
message = result.error.message || 'Unable to sign in. Please try again.';
return;
}
// Destination is a validated local path, prefixed by resolve('/') above.
// goto rejects paths that match no route, so fall back to a full page load.
await goto(destination, { refreshAll: true }).catch(() => {
window.location.href = destination;
});
} catch {
message = 'Unable to connect. Please try again.';
} finally {
busy = false;
}
}
async function social(provider: 'google' | 'osu') {
busy = true;
message = '';
try {
const result = await authClient.signIn.social({
provider,
callbackURL: returnTo,
errorCallbackURL: `/login?returnTo=${encodeURIComponent(returnTo)}`
});
if (result.error) {
message = result.error.message || 'Unable to connect to this provider.';
busy = false;
}
} catch {
message = 'Unable to connect. Please try again.';
busy = false;
}
}
</script>
<section class="mx-auto my-12 max-w-md px-4 sm:my-20">
<Card.Root>
<Card.Header
><Card.Title class="text-2xl">{mode === 'register' ? 'create account' : 'sign in'}</Card.Title
><Card.Description>Use your account to save and manage your work.</Card.Description
></Card.Header
>
<Card.Content>
{#if message || oauthError}
<p
role="alert"
class="mb-4 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive"
>
{message ||
'Provider sign-in failed or was cancelled. Try again, or use your existing login and link the provider from your account.'}
</p>
{/if}
<form method="POST" onsubmit={submit} class="flex flex-col gap-4">
{#if mode === 'register'}
<div class="space-y-2">
<Label for="auth-name">Name</Label><Input
id="auth-name"
disabled={!ready || busy}
name="name"
autocomplete="name"
required
maxlength={100}
bind:value={name}
/>
</div>
{/if}
<div class="space-y-2">
<Label for="auth-email">Email</Label><Input
id="auth-email"
disabled={!ready || busy}
name="email"
type="email"
autocomplete="email"
required
bind:value={email}
/>
</div>
<div class="space-y-2">
<Label for="auth-password">Password</Label><Input
id="auth-password"
disabled={!ready || busy}
name="password"
type="password"
autocomplete={mode === 'register' ? 'new-password' : 'current-password'}
required
minlength={mode === 'register' ? 12 : undefined}
maxlength={128}
bind:value={password}
/>{#if mode === 'register'}<p class="text-sm text-muted-foreground">
Use at least 12 characters.
</p>{/if}
</div>
<Button type="submit" disabled={!ready || busy} class="w-full"
>{busy ? 'please wait…' : mode === 'register' ? 'create account' : 'sign in'}</Button
>
</form>
{#if providers.google || providers.osu}
<div class="my-6 flex flex-col gap-3">
<Separator />
{#if providers.google}<Button
variant="outline"
disabled={!ready || busy}
onclick={() => social('google')}>Continue with Google</Button
>{/if}
{#if providers.osu}<Button
variant="outline"
disabled={!ready || busy}
onclick={() => social('osu')}>Continue with osu!</Button
>{/if}
</div>
{/if}
<p class="mt-6 text-base">
<a
class="underline underline-offset-4"
href={resolve(
`${mode === 'register' ? '/login' : '/register'}?returnTo=${encodeURIComponent(returnTo)}`
)}>{mode === 'register' ? 'sign in' : 'Create account'}</a
>
</p>
<noscript><p class="mt-4">Enable JavaScript to securely sign in or register.</p></noscript>
</Card.Content>
</Card.Root>
</section>