auth-yes/public/webauthn-register.js
Tyler Gillispie 8b30b17dbe
feat(arch): implement Phase 2 base vertical slices and shared UI (#57)
- Scaffold shared UI fragments and styles in src/shared/ui/
- Implement full Auth vertical slice in src/features/auth/ with WebAuthn ceremonies and recovery endpoints
- Implement full Admin vertical slice in src/features/admin/ with responsive tables and mobile decks
- Add public client-side JS utilities and pure JS BIP-39 module
- Mount routes in src/main.ts and keep legacy server/ and ui/ quarantined
- Add pure JSX and API tests for Auth and Admin slices
2026-08-27 19:29:08 -07:00

99 lines
3.2 KiB
JavaScript

import { entropyToMnemonic } from "/public/utils/bip39.js";
const urlParams = new URLSearchParams(globalThis.location.search);
const codeParam = urlParams.get("code");
if (codeParam) {
const input = document.getElementById("inviteCode");
if (input) {
input.value = codeParam;
document.getElementById("username")?.focus();
}
}
let generatedMnemonic = "";
const registerBtn = document.getElementById("registerBtn");
if (registerBtn) {
registerBtn.addEventListener("click", async () => {
const username = document.getElementById("username").value.trim();
const inviteCode = document.getElementById("inviteCode").value.trim();
if (!username || !inviteCode) {
setStatus("Username and Invite Code are required.", true);
return;
}
const loader = document.getElementById("loadingIndicator");
const btn = document.getElementById("registerBtn");
loader.style.display = "block";
btn.disabled = true;
setStatus("");
try {
const res = await startWebAuthnRegistration(username, inviteCode);
if (res && res.success) {
// Generate 16 bytes of entropy for 12 BIP-39 words
const entropy = new Uint8Array(16);
crypto.getRandomValues(entropy);
generatedMnemonic = await entropyToMnemonic(entropy);
const words = generatedMnemonic.split(" ");
const grid = document.getElementById("wordGrid");
grid.innerHTML = words.map((w, idx) => `
<div class="word-cell">
<span class="word-num">${idx + 1}.</span>
<span class="word-text">${w}</span>
</div>
`).join("");
// Switch to Step 2
document.getElementById("step1Container").style.display = "none";
document.getElementById("step2Container").style.display = "block";
document.getElementById("registerTitle").textContent =
"Recovery Voucher";
document.getElementById("registerSubtitle").textContent =
"Save your 12-word backup key in a safe place.";
}
} catch (err) {
console.error(err);
} finally {
loader.style.display = "none";
btn.disabled = false;
}
});
}
const copyWordsBtn = document.getElementById("copyWordsBtn");
if (copyWordsBtn) {
copyWordsBtn.addEventListener("click", async () => {
if (!generatedMnemonic) return;
try {
await navigator.clipboard.writeText(generatedMnemonic);
const btn = document.getElementById("copyWordsBtn");
const origHtml = btn.innerHTML;
btn.innerHTML = "<span>✓ Copied to Clipboard!</span>";
btn.style.borderColor = "var(--success)";
btn.style.color = "var(--success-text)";
setTimeout(() => {
btn.innerHTML = origHtml;
btn.style.borderColor = "";
btn.style.color = "";
}, 2500);
} catch (_e) {
alert("Select and copy the words manually: " + generatedMnemonic);
}
});
}
function setStatus(msg, isError) {
const statusDiv = document.getElementById("statusMessage");
if (!statusDiv) return;
if (msg) {
statusDiv.textContent = msg;
statusDiv.className = isError ? "error" : "success";
statusDiv.style.display = "block";
} else {
statusDiv.style.display = "none";
}
}