Some checks failed
Test and deploy staging / staging (push) Failing after 43s
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>
164 lines
4.9 KiB
Svelte
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>
|