Files
shd.build/src/routes/sign-in/+page.svelte
T

73 lines
5.3 KiB
Svelte

<script lang="ts">
import PendingForm from '$lib/components/PendingForm.svelte';
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
import { onMount } from 'svelte';
let { data, form } = $props();
let discordPending = $state(false);
let passkeyPending = $state(false);
let passkeySupported = $state(true);
let passkeyError = $state('');
onMount(() => passkeySupported = browserSupportsWebAuthn());
async function signInWithPasskey() {
if (passkeyPending) return;
passkeyPending = true;
passkeyError = '';
try {
const optionsRequest = await fetch('/auth/passkey/authenticate/options', { method: 'POST' });
if (!optionsRequest.ok) throw new Error('Could not start passkey sign-in.');
const { options, challengeId } = await optionsRequest.json();
const response = await startAuthentication({ optionsJSON: options });
const verification = await fetch('/auth/passkey/authenticate/verify', {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ challengeId, response })
});
const result = await verification.json();
if (!verification.ok || !result.verified) throw new Error(result.message || 'Passkey sign-in failed.');
location.href = result.redirectTo || '/account';
} catch (error) {
passkeyError = error instanceof Error ? error.message : 'Passkey sign-in was cancelled.';
passkeyPending = false;
}
}
function beginDiscordSignIn(event: MouseEvent) {
if (discordPending) {
event.preventDefault();
return;
}
discordPending = true;
}
</script>
<svelte:head><title>Sign in — SHD.build</title></svelte:head>
<main class="auth-page"><div class="auth-frame"><section class="auth-intel"><p class="eyebrow"><span></span> SHD IDENTITY NETWORK</p><h1>AGENT<br/><em>ACCESS.</em></h1><p>Authenticate to publish builds, join field discussions, and keep your loadouts synchronized across devices.</p><div class="auth-status"><span><i></i> NETWORK ONLINE</span><span>NODE NYC-01</span></div></section><section class="auth-console"><div class="console-head"><span>SECURE SIGN-IN</span><b>SHD // AUTH</b></div>
<button class="primary full passkey-signin" disabled={!passkeySupported || passkeyPending} aria-busy={passkeyPending} onclick={signInWithPasskey}>{passkeyPending ? 'VERIFYING PASSKEY…' : 'SIGN IN WITH A PASSKEY →'}</button>
{#if !passkeySupported}<p class="auth-note">This browser does not support passkeys.</p>{/if}
{#if passkeyError}<p class="error">{passkeyError}</p>{/if}
<div class="divider"><span>OR EMAIL A ONE-TIME LINK</span></div>
<PendingForm action="?/magicLink"><label class="field">VERIFIED EMAIL<input name="email" type="email" value={form?.email ?? ''} autocomplete="email" placeholder="agent@example.com" required /></label>{#if form?.magicLinkError}<p class="error">{form.magicLinkError}</p>{/if}{#if form?.magicLinkSent || data.magicLinkSent}<p class="success">If that email is linked to an agent, a sign-in link is on its way.</p>{/if}{#if data.magicError}<p class="error">{data.magicError}</p>{/if}<button class="secondary full" disabled={!data.emailDeliveryConfigured}>EMAIL MAGIC LINK →</button></PendingForm>
<div class="divider"><span>OR CONTINUE WITH</span></div>
<div class="signin-provider-rail" aria-label="External sign-in providers">
<a class="signin-provider discord-provider" class:interaction-pending={discordPending} href="/auth/discord" aria-busy={discordPending} onclick={beginDiscordSignIn}>
<span class="signin-provider-icon"><img src="/discord-symbol.svg" alt="" aria-hidden="true" /></span>
<span>Discord</span>
</a>
<button class="signin-provider google-signin-provider" type="button" disabled title="Google sign-in is planned" aria-label="Google sign-in — planned">
<span class="signin-provider-icon"><img src="/google-g.svg" alt="" aria-hidden="true" /></span>
<span>Google</span>
<em>Planned</em>
</button>
<button class="signin-provider apple-signin-provider" type="button" disabled title="Apple sign-in is planned" aria-label="Apple sign-in — planned">
<span class="signin-provider-icon"><img src="/apple-mark.svg" alt="" aria-hidden="true" /></span>
<span>Apple</span>
<em>Planned</em>
</button>
<button class="signin-provider steam-signin-provider" type="button" disabled title="Steam sign-in is planned" aria-label="Steam sign-in — planned">
<span class="signin-provider-icon"><img src="/steam-mark.svg" alt="" aria-hidden="true" /></span>
<span>Steam</span>
<em>Planned</em>
</button>
</div>
{#if data.oauthError}<p class="error">{data.oauthError}</p>{/if}
<details class="password-fallback" open={Boolean(form?.loginError)}><summary>USE USERNAME AND PASSWORD</summary><PendingForm action="?/login"><label class="field">USERNAME<input name="username" value={form?.username ?? ''} autocomplete="username webauthn" placeholder="Agent handle" required /></label><label class="field">PASSWORD<input name="password" type="password" autocomplete="current-password" placeholder="••••••••••••" required /></label>{#if form?.loginError}<p class="error">{form.loginError}</p>{/if}<button class="secondary full">SIGN IN WITH PASSWORD →</button></PendingForm></details>
<small>Passkeys and one-time email links are preferred. Password sign-in remains available as an optional fallback.</small>
</section></div></main>