73 lines
5.3 KiB
Svelte
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>
|