// Passkey management for settings page import { bufferToBase64url, base64urlToBuffer, getCsrfToken } from './webauthn_utils' document.addEventListener('DOMContentLoaded', () => { const addPasskeyBtn = document.getElementById('add-passkey-btn') const modal = document.getElementById('passkey-modal') const nameInput = document.getElementById('passkey-name-input') const confirmBtn = document.getElementById('confirm-add-passkey') const cancelBtn = document.getElementById('cancel-add-passkey') const errorDiv = document.getElementById('passkey-error') if (!addPasskeyBtn || !modal) return // Show modal when clicking "Add Passkey" addPasskeyBtn.addEventListener('click', () => { modal.classList.remove('hidden') nameInput.value = '' nameInput.focus() hideError() }) // Hide modal when clicking cancel or background const hideModal = () => { modal.classList.add('hidden') nameInput.value = '' hideError() } cancelBtn.addEventListener('click', hideModal) modal.addEventListener('click', (e) => { if (e.target === modal || e.target.classList.contains('bg-zinc-500')) { hideModal() } }) // Handle escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !modal.classList.contains('hidden')) { hideModal() } }) // Show error in modal const showError = (message) => { errorDiv.classList.remove('hidden') errorDiv.querySelector('p').textContent = message } const hideError = () => { errorDiv.classList.add('hidden') errorDiv.querySelector('p').textContent = '' } // Handle passkey registration const registerPasskey = async () => { const name = nameInput.value.trim() if (!name) { showError('Please enter a name for this passkey') nameInput.focus() return } try { confirmBtn.disabled = true confirmBtn.textContent = 'Registering...' // Fetch challenge from server const challengeResponse = await fetch('/api/webauthn/registration/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() } }) if (!challengeResponse.ok) { const error = await challengeResponse.json() showError(error.error || 'Failed to get registration challenge') return } const challengeData = await challengeResponse.json() // Convert challenge and user ID to ArrayBuffer const challenge = base64urlToBuffer(challengeData.challenge) const userId = base64urlToBuffer(challengeData.user.id) const publicKeyCredentialCreationOptions = { challenge, rp: challengeData.rp, user: { id: userId, name: challengeData.user.name, displayName: challengeData.user.displayName }, pubKeyCredParams: challengeData.pubKeyCredParams, authenticatorSelection: challengeData.authenticatorSelection, timeout: challengeData.timeout, attestation: challengeData.attestation } // Hide modal before showing authenticator prompt modal.classList.add('hidden') // Create credential const credential = await navigator.credentials.create({ publicKey: publicKeyCredentialCreationOptions }) const registrationData = { name: name, attestation: { id: credential.id, rawId: bufferToBase64url(credential.rawId), type: credential.type, response: { clientDataJSON: bufferToBase64url(credential.response.clientDataJSON), attestationObject: bufferToBase64url(credential.response.attestationObject) } } } // Send to server const registerResponse = await fetch('/api/webauthn/register', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() }, body: JSON.stringify(registrationData) }) if (registerResponse.ok) { // Reload page to show new passkey window.location.reload() } else { const error = await registerResponse.json() modal.classList.remove('hidden') showError(error.error || 'Failed to register passkey') } } catch (error) { console.error('Registration error:', error) let errorMessage = 'Failed to register passkey' if (error.name === 'NotAllowedError') { errorMessage = 'Registration was cancelled or timed out' } else if (error.message) { errorMessage = error.message } modal.classList.remove('hidden') showError(errorMessage) } finally { confirmBtn.disabled = false confirmBtn.textContent = 'Continue' } } // Register on button click confirmBtn.addEventListener('click', registerPasskey) // Register on Enter key nameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault() registerPasskey() } }) })