server and web app

This commit is contained in:
2026-05-31 23:08:53 -04:00
parent 438b3b29a2
commit b3364447a1
24 changed files with 2318 additions and 153 deletions

View File

@@ -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);
}
});