| ... | @@ -10,12 +10,13 @@ export function SignIn() { | ... | @@ -10,12 +10,13 @@ export function SignIn() { |
| 10 | const setup = params.get("setup"); | 10 | const setup = params.get("setup"); |
| 11 | const statusQuery = new URLSearchParams(); | 11 | const statusQuery = new URLSearchParams(); |
| 12 | if (setup) statusQuery.set("setup", setup); | 12 | if (setup) statusQuery.set("setup", setup); |
| | 13 | if (params.get("flow")) statusQuery.set("flow", params.get("flow")!); |
| 13 | if (params.get("next")) statusQuery.set("next", params.get("next")!); | 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 | const [username, setUsername] = createSignal(""); | 16 | const [username, setUsername] = createSignal(""); |
| 16 | const [password, setPassword] = createSignal(""); | 17 | const [password, setPassword] = createSignal(""); |
| 17 | const [email, setEmail] = createSignal(""); | 18 | const [email, setEmail] = createSignal(""); |
| 18 | const [remember, setRemember] = createSignal(false); | 19 | const [passwordStep, setPasswordStep] = createSignal(false); |
| 19 | const [visible, setVisible] = createSignal(false); | 20 | const [visible, setVisible] = createSignal(false); |
| 20 | const [busy, setBusy] = createSignal(false); | 21 | const [busy, setBusy] = createSignal(false); |
| 21 | const [error, setError] = createSignal(params.has("guest_error") ? "Guest sign-in wasn't completed. Choose a provider to try again." : ""); | 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,7 +24,7 @@ export function SignIn() { |
| 23 | let conditional: AbortController | undefined; | 24 | let conditional: AbortController | undefined; |
| 24 | let disposed = false; | 25 | let disposed = false; |
| 25 | const body = () => ({ csrf: status()!.csrf, username: username(), password: password(), email: email(), setup, | 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 | const passkey = async (automatic = false) => { | 28 | const passkey = async (automatic = false) => { |
| 28 | if (!status() || busy()) return; | 29 | if (!status() || busy()) return; |
| 29 | conditional?.abort(); | 30 | conditional?.abort(); |
| ... | @@ -41,20 +42,27 @@ export function SignIn() { | ... | @@ -41,20 +42,27 @@ export function SignIn() { |
| 41 | }); | 42 | }); |
| 42 | if (!(credential instanceof PublicKeyCredential)) throw new Error("Passkey sign-in was canceled. Try again or use your password."); | 43 | if (!(credential instanceof PublicKeyCredential)) throw new Error("Passkey sign-in was canceled. Try again or use your password."); |
| 43 | selected = true; setBusy(true); | 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 | window.location.assign(result.next); | 46 | window.location.assign(result.next); |
| 46 | } catch (failure) { | 47 | } catch (failure) { |
| 47 | if ((!automatic || selected) && !controller.signal.aborted) setError(authReason(failure)); | 48 | if ((!automatic || selected) && !controller.signal.aborted) setError(authReason(failure)); |
| 48 | } finally { if (!automatic || selected) setBusy(false); } | 49 | } finally { if (!automatic || selected) setBusy(false); } |
| 49 | }; | 50 | }; |
| 50 | createEffect(() => { | 51 | createEffect(() => { |
| 51 | if (!setup && status()) void (async () => { | 52 | if (!setup && !passwordStep() && status()) void (async () => { |
| 52 | if (await PublicKeyCredential.isConditionalMediationAvailable?.() && !disposed) await passkey(true); | 53 | if (await PublicKeyCredential.isConditionalMediationAvailable?.() && !disposed && !passwordStep()) await passkey(true); |
| 53 | })().catch(() => {}); | 54 | })().catch(() => {}); |
| 54 | }); | 55 | }); |
| 55 | const complete = async () => { | 56 | const complete = async () => { |
| 56 | if (!status() || busy()) return; | 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 | try { | 66 | try { |
| 59 | const result = await authRequest<{ next: string }>(setup ? "setup" : "password", body()); | 67 | const result = await authRequest<{ next: string }>(setup ? "setup" : "password", body()); |
| 60 | window.location.assign(result.next); | 68 | window.location.assign(result.next); |
| ... | @@ -73,18 +81,25 @@ export function SignIn() { | ... | @@ -73,18 +81,25 @@ export function SignIn() { |
| 73 | <main class="pf-v5-c-login__main"> | 81 | <main class="pf-v5-c-login__main"> |
| 74 | <div id="kc-header"><div id="kc-header-wrapper"><div class="kc-logo-text"><span>snow sign on</span></div></div></div> | 82 | <div id="kc-header"><div id="kc-header-wrapper"><div class="kc-logo-text"><span>snow sign on</span></div></div></div> |
| 75 | <div class="card-pf"> | 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 | <div id="kc-content"><div id="kc-content-wrapper"> | 85 | <div id="kc-content"><div id="kc-content-wrapper"> |
| 78 | <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> | 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 | <Show when={notice()}><div class="alert-info pf-v5-c-alert" role="status"><span class="pf-v5-c-alert__title">{notice()}</span></div></Show> | 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 | <Show when={!status.error} fallback={<button class="pf-v5-c-button pf-m-primary pf-m-block" onClick={() => refetch()}>try again</button>}> | 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 | <div id="kc-form"><div id="kc-form-wrapper"> | 94 | <div id="kc-form"><div id="kc-form-wrapper"> |
| 82 | <form id="kc-form-login" onSubmit={(event) => { event.preventDefault(); void complete(); }}> | 95 | <form id="kc-form-login" onSubmit={(event) => { event.preventDefault(); void complete(); }}> |
| 83 | <Show when={setup} fallback={ | 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 | <div class="pf-v5-c-form__group"> | 98 | <div class="pf-v5-c-form__group"> |
| 85 | <label for="username" class="pf-v5-c-form__label">username or email</label> | 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 | </div> | 101 | </div> |
| | 102 | </Show> |
| 88 | }> | 103 | }> |
| 89 | <p class="setup-intro">Your account is <b>{status()?.setup ?? "…"}</b>. Add your email and choose a password.</p> | 104 | <p class="setup-intro">Your account is <b>{status()?.setup ?? "…"}</b>. Add your email and choose a password.</p> |
| 90 | <div class="pf-v5-c-form__group"> | 105 | <div class="pf-v5-c-form__group"> |
| ... | @@ -92,29 +107,26 @@ export function SignIn() { | ... | @@ -92,29 +107,26 @@ export function SignIn() { |
| 92 | <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 /> | 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 | </div> | 108 | </div> |
| 94 | </Show> | 109 | </Show> |
| | 110 | <Show when={setup || passwordStep()}> |
| 95 | <div class="pf-v5-c-form__group"> | 111 | <div class="pf-v5-c-form__group"> |
| 96 | <label for="password" class="pf-v5-c-form__label">{setup ? "choose a password" : "password"}</label> | 112 | <label for="password" class="pf-v5-c-form__label">{setup ? "choose a password" : "password"}</label> |
| 97 | <div class="pf-v5-c-input-group"> | 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 | <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> | 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 | </div> | 116 | </div> |
| 101 | <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> | 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 | </div> | 118 | </div> |
| 103 | <Show when={!setup}><div class="pf-v5-c-form__group"> | 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 | <div><span><a href="#" onClick={(event) => { event.preventDefault(); setNotice("Ask Clover for a password reset link."); }}>forgot password?</a></span></div> | 120 | <div><span><a href="#" onClick={(event) => { event.preventDefault(); setNotice("Ask Clover for a password reset link."); }}>forgot password?</a></span></div> |
| 106 | </div></Show> | 121 | </div></Show> |
| | 122 | </Show> |
| 107 | <div id="kc-form-buttons" class="pf-v5-c-form__group"> | 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 | </div> | 125 | </div> |
| 110 | </form> | 126 | </form> |
| 111 | </div></div> | 127 | </div></div> |
| 112 | <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> | 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}> | 129 | |
| 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> | | |
| 118 | </Show> | 130 | </Show> |
| 119 | </div></div> | 131 | </div></div> |
| 120 | </div> | 132 | </div> |