server and web app
This commit is contained in:
@@ -1,4 +1,56 @@
|
||||
(() => {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.auth-tab-list').forEach(tabList => {
|
||||
const tabs = tabList.querySelectorAll('[data-tab]');
|
||||
const panels = tabList.closest('.auth-tabs').querySelectorAll('[data-tab-panel]');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const target = tab.dataset.tab;
|
||||
|
||||
tabs.forEach(t => {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
|
||||
panels.forEach(p => {
|
||||
if (p.dataset.tabPanel === target) {
|
||||
p.classList.add('is-active');
|
||||
p.hidden = false;
|
||||
} else {
|
||||
p.classList.remove('is-active');
|
||||
p.hidden = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Register toggle
|
||||
const registerToggle = document.querySelector('[data-register-toggle]');
|
||||
const registerPanel = document.querySelector('[data-register-panel]');
|
||||
const loginSection = document.querySelector('[data-login-section]');
|
||||
const registerCancel = document.querySelector('[data-register-cancel]');
|
||||
|
||||
if (registerToggle && registerPanel && loginSection) {
|
||||
registerToggle.addEventListener('click', () => {
|
||||
loginSection.hidden = true;
|
||||
registerPanel.hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (registerCancel && registerPanel && loginSection) {
|
||||
registerCancel.addEventListener('click', () => {
|
||||
registerPanel.hidden = true;
|
||||
loginSection.hidden = false;
|
||||
// Clear any registration form status messages
|
||||
document.querySelector('[data-passkey-register-status]').textContent = '';
|
||||
document.querySelector('[data-register-status]').textContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
const jsonHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
const postJSON = async (url, body, options = {}) => {
|
||||
@@ -95,35 +147,76 @@
|
||||
}
|
||||
});
|
||||
|
||||
const webAuthnErrorMessage = (error) => {
|
||||
console.error("WebAuthn error:", error.name, error.message, error);
|
||||
switch (error.name) {
|
||||
case "NotAllowedError":
|
||||
return "Passkey creation was cancelled or no authenticator is available. If you dismissed a prompt, try again. If no prompt appeared, your device may not support passkeys.";
|
||||
case "NotSupportedError":
|
||||
return "This browser or device does not support passkeys.";
|
||||
case "SecurityError":
|
||||
return "Passkeys require a secure context. Use https:// or localhost.";
|
||||
case "InvalidStateError":
|
||||
return "A passkey may already exist for this account.";
|
||||
default:
|
||||
return error.message || "Passkey failed. Check the console for details.";
|
||||
}
|
||||
};
|
||||
|
||||
const checkPasskeySupport = async () => {
|
||||
if (!window.PublicKeyCredential) {
|
||||
return { supported: false, reason: "Passkeys are not available in this browser." };
|
||||
}
|
||||
try {
|
||||
if (PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
|
||||
const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||
if (!available) {
|
||||
return { supported: true, reason: "No platform authenticator found (e.g., Touch ID, Windows Hello). You can still use a security key if you have one." };
|
||||
}
|
||||
}
|
||||
return { supported: true };
|
||||
} catch {
|
||||
return { supported: true };
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector("[data-passkey-register]")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!window.PublicKeyCredential) {
|
||||
setStatus("[data-passkey-register-status]", "Passkeys are not available in this browser.", true);
|
||||
const support = await checkPasskeySupport();
|
||||
if (!support.supported) {
|
||||
setStatus("[data-passkey-register-status]", support.reason, true);
|
||||
return;
|
||||
}
|
||||
if (support.reason) {
|
||||
console.warn("Passkey support:", support.reason);
|
||||
}
|
||||
try {
|
||||
const begin = await postJSON("/api/auth/passkeys/register/begin", formJSON(event.currentTarget));
|
||||
const credential = await navigator.credentials.create(normalizeCreateOptions(begin.options));
|
||||
await postJSON(`/api/auth/passkeys/register/finish?challengeId=${encodeURIComponent(begin.challengeId)}`, credentialToJSON(credential));
|
||||
setStatus("[data-passkey-register-status]", "Passkey created. Sign in with your passkey.");
|
||||
} catch (error) {
|
||||
setStatus("[data-passkey-register-status]", error.message, true);
|
||||
setStatus("[data-passkey-register-status]", webAuthnErrorMessage(error), true);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("[data-passkey-login]")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!window.PublicKeyCredential) {
|
||||
setStatus("[data-passkey-login-status]", "Passkeys are not available in this browser.", true);
|
||||
const support = await checkPasskeySupport();
|
||||
if (!support.supported) {
|
||||
setStatus("[data-passkey-login-status]", support.reason, true);
|
||||
return;
|
||||
}
|
||||
if (support.reason) {
|
||||
console.warn("Passkey support:", support.reason);
|
||||
}
|
||||
try {
|
||||
const begin = await postJSON("/api/auth/passkeys/login/begin", formJSON(event.currentTarget));
|
||||
const credential = await navigator.credentials.get(normalizeRequestOptions(begin.options));
|
||||
await postJSON(`/api/auth/passkeys/login/finish?challengeId=${encodeURIComponent(begin.challengeId)}`, credentialToJSON(credential));
|
||||
window.location.href = authNext();
|
||||
} catch (error) {
|
||||
setStatus("[data-passkey-login-status]", error.message, true);
|
||||
setStatus("[data-passkey-login-status]", webAuthnErrorMessage(error), true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user