| ... | ... | @@ -10,12 +10,13 @@ export function SignIn() { |
| 10 | 10 | const setup = params.get("setup"); |
| 11 | 11 | const statusQuery = new URLSearchParams(); |
| 12 | 12 | if (setup) statusQuery.set("setup", setup); |
| 13 | if (params.get("flow")) statusQuery.set("flow", params.get("flow")!); |
| 13 | 14 | if (params.get("next")) statusQuery.set("next", params.get("next")!); |
| 14 | | const [status, { refetch }] = createResource(() => authRequest<{ csrf: string; setup?: string; providers?: { id: string; name: string }[] }>(`status?${statusQuery}`)); |
| 15 | const [status, { refetch }] = createResource(() => authRequest<{ csrf: string; service: string; setup?: string; providers?: { id: string; name: string }[] }>(`status?${statusQuery}`)); |
| 15 | 16 | const [username, setUsername] = createSignal(""); |
| 16 | 17 | const [password, setPassword] = createSignal(""); |
| 17 | 18 | const [email, setEmail] = createSignal(""); |
| 18 | | const [remember, setRemember] = createSignal(false); |
| 19 | const [passwordStep, setPasswordStep] = createSignal(false); |
| 19 | 20 | const [visible, setVisible] = createSignal(false); |
| 20 | 21 | const [busy, setBusy] = createSignal(false); |
| 21 | 22 | const [error, setError] = createSignal(params.has("guest_error") ? "Guest sign-in wasn't completed. Choose a provider to try again." : ""); |
| ... | ... | @@ -23,7 +24,7 @@ export function SignIn() { |
| 23 | 24 | let conditional: AbortController | undefined; |
| 24 | 25 | let disposed = false; |
| 25 | 26 | const body = () => ({ csrf: status()!.csrf, username: username(), password: password(), email: email(), setup, |
| 26 | | remember: remember(), flow: params.get("flow") ?? "", next: params.get("next") ?? "/" }); |
| 27 | remember: false, flow: params.get("flow") ?? "", next: params.get("next") ?? "/" }); |
| 27 | 28 | const passkey = async (automatic = false) => { |
| 28 | 29 | if (!status() || busy()) return; |
| 29 | 30 | conditional?.abort(); |
| ... | ... | @@ -41,20 +42,27 @@ export function SignIn() { |
| 41 | 42 | }); |
| 42 | 43 | if (!(credential instanceof PublicKeyCredential)) throw new Error("Passkey sign-in was canceled. Try again or use your password."); |
| 43 | 44 | selected = true; setBusy(true); |
| 44 | | const result = await authRequest<{ next: string }>("passkey/finish", { csrf: request.csrf, token, remember: remember(), credential: credential.toJSON() }); |
| 45 | const result = await authRequest<{ next: string }>("passkey/finish", { csrf: request.csrf, token, remember: false, credential: credential.toJSON() }); |
| 45 | 46 | window.location.assign(result.next); |
| 46 | 47 | } catch (failure) { |
| 47 | 48 | if ((!automatic || selected) && !controller.signal.aborted) setError(authReason(failure)); |
| 48 | 49 | } finally { if (!automatic || selected) setBusy(false); } |
| 49 | 50 | }; |
| 50 | 51 | createEffect(() => { |
| 51 | | if (!setup && status()) void (async () => { |
| 52 | | if (await PublicKeyCredential.isConditionalMediationAvailable?.() && !disposed) await passkey(true); |
| 52 | if (!setup && !passwordStep() && status()) void (async () => { |
| 53 | if (await PublicKeyCredential.isConditionalMediationAvailable?.() && !disposed && !passwordStep()) await passkey(true); |
| 53 | 54 | })().catch(() => {}); |
| 54 | 55 | }); |
| 55 | 56 | const complete = async () => { |
| 56 | 57 | if (!status() || busy()) return; |
| 57 | | conditional?.abort(); setBusy(true); setError(""); setNotice(""); |
| 58 | conditional?.abort(); setError(""); setNotice(""); |
| 59 | if (!setup && !passwordStep()) { |
| 60 | if (!username().trim()) return; |
| 61 | setUsername(username().trim()); |
| 62 | setPasswordStep(true); |
| 63 | return; |
| 64 | } |
| 65 | setBusy(true); |
| 58 | 66 | try { |
| 59 | 67 | const result = await authRequest<{ next: string }>(setup ? "setup" : "password", body()); |
| 60 | 68 | window.location.assign(result.next); |
| ... | ... | @@ -73,18 +81,25 @@ export function SignIn() { |
| 73 | 81 | <main class="pf-v5-c-login__main"> |
| 74 | 82 | <div id="kc-header"><div id="kc-header-wrapper"><div class="kc-logo-text"><span>snow sign on</span></div></div></div> |
| 75 | 83 | <div class="card-pf"> |
| 76 | | <header><h1 id="kc-page-title">{setup ? "welcome" : "sign in to your account"}</h1></header> |
| 84 | <header><h1 id="kc-page-title">{setup ? "welcome" : `sign in to ${status()?.service?.toLowerCase() ?? "snow globe"}`}</h1></header> |
| 77 | 85 | <div id="kc-content"><div id="kc-content-wrapper"> |
| 78 | 86 | <Show when={status.error || error()}><div class="alert-error pf-v5-c-alert" role="alert"><span class="pf-v5-c-alert__title">{status.error ? authReason(status.error) : error()}</span></div></Show> |
| 79 | 87 | <Show when={notice()}><div class="alert-info pf-v5-c-alert" role="status"><span class="pf-v5-c-alert__title">{notice()}</span></div></Show> |
| 80 | 88 | <Show when={!status.error} fallback={<button class="pf-v5-c-button pf-m-primary pf-m-block" onClick={() => refetch()}>try again</button>}> |
| 89 | <Show when={!setup && status()?.providers?.length}> |
| 90 | <div id="kc-social-providers" class="guest-providers"> |
| 91 | <For each={status()?.providers}>{(provider) => <a rel="external" data-provider={provider.id} class="pf-v5-c-button pf-m-primary pf-m-block" href={`/auth/guest/start/${provider.id}?next=${encodeURIComponent(params.get("next") ?? "")}`}>{provider.id === "astheno" ? <Stone size={18} aria-hidden="true" /> : <img src={githubIcon} width="18" height="18" alt="" />}<span>continue with {provider.name}</span></a>}</For> |
| 92 | </div> |
| 93 | </Show> |
| 81 | 94 | <div id="kc-form"><div id="kc-form-wrapper"> |
| 82 | 95 | <form id="kc-form-login" onSubmit={(event) => { event.preventDefault(); void complete(); }}> |
| 83 | 96 | <Show when={setup} fallback={ |
| 97 | <Show when={!passwordStep()} fallback={<div class="sign-in-account"><span>{username()}</span><button type="button" onClick={() => { setPassword(""); setVisible(false); setError(""); setNotice(""); setPasswordStep(false); }}>change</button><input name="username" type="hidden" autocomplete="username" value={username()} /></div>}> |
| 84 | 98 | <div class="pf-v5-c-form__group"> |
| 85 | 99 | <label for="username" class="pf-v5-c-form__label">username or email</label> |
| 86 | | <input id="username" class="pf-v5-c-form-control" name="username" type="text" required autocomplete="username webauthn" value={username()} onInput={(event) => setUsername(event.currentTarget.value)} autofocus /> |
| 100 | <input id="username" class="pf-v5-c-form-control" name="username" type="text" required autocomplete="username webauthn" value={username()} onInput={(event) => setUsername(event.currentTarget.value)} autofocus ref={(input) => queueMicrotask(() => input.focus())} /> |
| 87 | 101 | </div> |
| 102 | </Show> |
| 88 | 103 | }> |
| 89 | 104 | <p class="setup-intro">Your account is <b>{status()?.setup ?? "…"}</b>. Add your email and choose a password.</p> |
| 90 | 105 | <div class="pf-v5-c-form__group"> |
| ... | ... | @@ -92,29 +107,26 @@ export function SignIn() { |
| 92 | 107 | <input id="email" class="pf-v5-c-form-control" name="email" type="email" required autocomplete="email" value={email()} onInput={(event) => setEmail(event.currentTarget.value)} autofocus /> |
| 93 | 108 | </div> |
| 94 | 109 | </Show> |
| 110 | <Show when={setup || passwordStep()}> |
| 95 | 111 | <div class="pf-v5-c-form__group"> |
| 96 | 112 | <label for="password" class="pf-v5-c-form__label">{setup ? "choose a password" : "password"}</label> |
| 97 | 113 | <div class="pf-v5-c-input-group"> |
| 98 | | <input id="password" class="pf-v5-c-form-control" name="password" type={visible() ? "text" : "password"} required minLength={setup ? 8 : undefined} maxLength={1024} autocomplete={setup ? "new-password" : "current-password"} value={password()} onInput={(event) => setPassword(event.currentTarget.value)} /> |
| 114 | <input id="password" class="pf-v5-c-form-control" name="password" type={visible() ? "text" : "password"} required minLength={setup ? 8 : undefined} maxLength={1024} autocomplete={setup ? "new-password" : "current-password"} value={password()} onInput={(event) => setPassword(event.currentTarget.value)} ref={(input) => { if (!setup) queueMicrotask(() => input.focus()); }} /> |
| 99 | 115 | <button class="pf-v5-c-button pf-m-control" type="button" aria-label={visible() ? "Hide password" : "Show password"} aria-controls="password" data-password-toggle onClick={() => setVisible(!visible())}><i aria-hidden="true" /></button> |
| 100 | 116 | </div> |
| 101 | 117 | <Show when={setup}><span class="pf-v5-c-helper-text__item-text">Use at least 8 characters. You can add a passkey next.</span></Show> |
| 102 | 118 | </div> |
| 103 | 119 | <Show when={!setup}><div class="pf-v5-c-form__group"> |
| 104 | | <div id="kc-form-options"><div class="checkbox"><label><input id="rememberMe" name="rememberMe" type="checkbox" checked={remember()} onChange={(event) => setRemember(event.currentTarget.checked)} /> remember me</label></div></div> |
| 105 | 120 | <div><span><a href="#" onClick={(event) => { event.preventDefault(); setNotice("Ask Clover for a password reset link."); }}>forgot password?</a></span></div> |
| 106 | 121 | </div></Show> |
| 122 | </Show> |
| 107 | 123 | <div id="kc-form-buttons" class="pf-v5-c-form__group"> |
| 108 | | <input class="pf-v5-c-button pf-m-primary pf-m-block" name="login" id="kc-login" type="submit" value={setup ? "create account" : "Sign In"} disabled={busy() || !status()} aria-busy={busy()} /> |
| 124 | <input class="pf-v5-c-button pf-m-primary pf-m-block" name="login" id="kc-login" type="submit" value={setup ? "create account" : "sign in"} disabled={busy() || !status()} aria-busy={busy()} /> |
| 109 | 125 | </div> |
| 110 | 126 | </form> |
| 111 | 127 | </div></div> |
| 112 | 128 | <Show when={!setup}><a id="authenticateWebAuthnButton" href="#" class="pf-v5-c-button pf-m-secondary pf-m-block" aria-disabled={busy() || !status()} onClick={(event) => { event.preventDefault(); void passkey(); }}>sign in with passkey</a></Show> |
| 113 | | <Show when={!setup && status()?.providers?.length}> |
| 114 | | <div id="kc-social-providers" class="guest-providers"> |
| 115 | | <For each={status()?.providers}>{(provider) => <a rel="external" data-provider={provider.id} class="pf-v5-c-button pf-m-primary pf-m-block" href={`/auth/guest/start/${provider.id}?next=${encodeURIComponent(params.get("next") ?? "")}`}>{provider.id === "astheno" ? <Stone size={18} aria-hidden="true" /> : <img src={githubIcon} width="18" height="18" alt="" />}<span>continue with {provider.name}</span></a>}</For> |
| 116 | | </div> |
| 117 | | </Show> |
| 129 | |
| 118 | 130 | </Show> |
| 119 | 131 | </div></div> |
| 120 | 132 | </div> |