fix(auth): username-first flow in rstack-identity sign-in modal
The actual login UI lives in rstack-identity.ts, not login-button.ts. Added username input to the sign-in modal, pass allowCredentials from server to WebAuthn so the browser auto-selects the matching passkey. Shows "No account found" if username not recognized. Enter key support and auto-focus on the username field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e9ca3ec30
commit
a83c714f5a
|
|
@ -640,6 +640,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
<button class="close-btn" data-action="cancel">×</button>
|
<button class="close-btn" data-action="cancel">×</button>
|
||||||
<h2>Sign up / Sign in</h2>
|
<h2>Sign up / Sign in</h2>
|
||||||
<p>Secure, passwordless authentication powered by passkeys.</p>
|
<p>Secure, passwordless authentication powered by passkeys.</p>
|
||||||
|
<input class="input" id="auth-signin-username" type="text" placeholder="Username or email" autocomplete="username webauthn" maxlength="64" />
|
||||||
<div class="actions actions--stack">
|
<div class="actions actions--stack">
|
||||||
<button class="btn btn--primary" data-action="signin">🔑 Sign In with Passkey</button>
|
<button class="btn btn--primary" data-action="signin">🔑 Sign In with Passkey</button>
|
||||||
<button class="btn btn--outline" data-action="switch-register">🔐 Create New Account</button>
|
<button class="btn btn--outline" data-action="switch-register">🔐 Create New Account</button>
|
||||||
|
|
@ -672,25 +673,47 @@ export class RStackIdentity extends HTMLElement {
|
||||||
const handleSignIn = async () => {
|
const handleSignIn = async () => {
|
||||||
const errEl = overlay.querySelector("#auth-error") as HTMLElement | null;
|
const errEl = overlay.querySelector("#auth-error") as HTMLElement | null;
|
||||||
const btn = overlay.querySelector('[data-action="signin"]') as HTMLButtonElement | null;
|
const btn = overlay.querySelector('[data-action="signin"]') as HTMLButtonElement | null;
|
||||||
|
const usernameInput = overlay.querySelector("#auth-signin-username") as HTMLInputElement | null;
|
||||||
|
const enteredUsername = usernameInput?.value.trim() || "";
|
||||||
|
const loginUsername = usernameHint || enteredUsername;
|
||||||
if (errEl) errEl.textContent = "";
|
if (errEl) errEl.textContent = "";
|
||||||
|
if (!loginUsername) {
|
||||||
|
if (errEl) errEl.textContent = "Please enter your username or email.";
|
||||||
|
usernameInput?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Authenticating...'; }
|
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Authenticating...'; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startRes = await fetch(`${ENCRYPTID_URL}/api/auth/start`, {
|
const startRes = await fetch(`${ENCRYPTID_URL}/api/auth/start`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(usernameHint ? { username: usernameHint } : {}),
|
body: JSON.stringify({ username: loginUsername }),
|
||||||
});
|
});
|
||||||
if (!startRes.ok) throw new Error("Failed to start authentication");
|
if (!startRes.ok) throw new Error("Failed to start authentication");
|
||||||
const { options: serverOptions } = await startRes.json();
|
const { options: serverOptions, userFound } = await startRes.json();
|
||||||
|
|
||||||
|
if (!userFound) {
|
||||||
|
throw new Error("No account found for that username. Try creating a new account.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build allowCredentials from server response to scope to this user's passkeys
|
||||||
|
const pubKeyOpts: any = {
|
||||||
|
challenge: new Uint8Array(base64urlToBuffer(serverOptions.challenge)),
|
||||||
|
rpId: serverOptions.rpId || "rspace.online",
|
||||||
|
userVerification: "required",
|
||||||
|
timeout: 60000,
|
||||||
|
};
|
||||||
|
if (serverOptions.allowCredentials?.length) {
|
||||||
|
pubKeyOpts.allowCredentials = serverOptions.allowCredentials.map((c: any) => ({
|
||||||
|
type: c.type,
|
||||||
|
id: new Uint8Array(base64urlToBuffer(c.id)),
|
||||||
|
...(c.transports?.length ? { transports: c.transports } : {}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const credential = (await navigator.credentials.get({
|
const credential = (await navigator.credentials.get({
|
||||||
publicKey: {
|
publicKey: pubKeyOpts,
|
||||||
challenge: new Uint8Array(base64urlToBuffer(serverOptions.challenge)),
|
|
||||||
rpId: serverOptions.rpId || "rspace.online",
|
|
||||||
userVerification: "required",
|
|
||||||
timeout: 60000,
|
|
||||||
},
|
|
||||||
})) as PublicKeyCredential;
|
})) as PublicKeyCredential;
|
||||||
if (!credential) throw new Error("Authentication failed");
|
if (!credential) throw new Error("Authentication failed");
|
||||||
|
|
||||||
|
|
@ -815,6 +838,9 @@ export class RStackIdentity extends HTMLElement {
|
||||||
overlay.querySelector("#auth-username")?.addEventListener("keydown", (e) => {
|
overlay.querySelector("#auth-username")?.addEventListener("keydown", (e) => {
|
||||||
if ((e as KeyboardEvent).key === "Enter") handleRegister();
|
if ((e as KeyboardEvent).key === "Enter") handleRegister();
|
||||||
});
|
});
|
||||||
|
overlay.querySelector("#auth-signin-username")?.addEventListener("keydown", (e) => {
|
||||||
|
if ((e as KeyboardEvent).key === "Enter") handleSignIn();
|
||||||
|
});
|
||||||
overlay.addEventListener("click", (e) => {
|
overlay.addEventListener("click", (e) => {
|
||||||
if (e.target === overlay) {
|
if (e.target === overlay) {
|
||||||
close();
|
close();
|
||||||
|
|
@ -829,6 +855,9 @@ export class RStackIdentity extends HTMLElement {
|
||||||
// If switching persona, auto-trigger sign-in immediately
|
// If switching persona, auto-trigger sign-in immediately
|
||||||
if (usernameHint) {
|
if (usernameHint) {
|
||||||
handleSignIn();
|
handleSignIn();
|
||||||
|
} else {
|
||||||
|
// Focus the username input
|
||||||
|
setTimeout(() => (overlay.querySelector("#auth-signin-username") as HTMLInputElement)?.focus(), 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue