// Passkey management for settings page import { bufferToBase64url, base64urlToBuffer, getCsrfToken } from './webauthn_utils' import type { WebAuthnChallengeData, ErrorResponse } from './types/webauthn' document.addEventListener('DOMContentLoaded', () => { const addPasskeyBtn = document.getElementById('add-passkey-btn') const modal = document.getElementById('passkey-modal') const nameInput = document.getElementById('passkey-name-input') as HTMLInputElement | null const confirmBtn = document.getElementById('confirm-add-passkey') as HTMLButtonElement | null const cancelBtn = document.getElementById('cancel-add-passkey') const errorDiv = document.getElementById('passkey-error') if (!addPasskeyBtn || !modal || !nameInput || !confirmBtn || !cancelBtn || !errorDiv) 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 as HTMLElement).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: string) => { errorDiv.classList.remove('hidden') const errorText = errorDiv.querySelector('p') if (errorText) { errorText.textContent = message } } const hideError = () => { errorDiv.classList.add('hidden') const errorText = errorDiv.querySelector('p') if (errorText) { errorText.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: ErrorResponse = await challengeResponse.json() showError(error.error || 'Failed to get registration challenge') return } const challengeData: WebAuthnChallengeData = await challengeResponse.json() // Convert challenge and user ID to ArrayBuffer const challenge = base64urlToBuffer(challengeData.challenge) const userId = base64urlToBuffer(challengeData.user.id) const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = { challenge, rp: challengeData.rp, user: { id: userId, name: challengeData.user.name, displayName: challengeData.user.displayName }, pubKeyCredParams: challengeData.pubKeyCredParams as PublicKeyCredentialParameters[], authenticatorSelection: challengeData.authenticatorSelection, timeout: challengeData.timeout, attestation: (challengeData.attestation as AttestationConveyancePreference) || 'none' } // Hide modal before showing authenticator prompt modal.classList.add('hidden') // Create credential const credential = await navigator.credentials.create({ publicKey: publicKeyCredentialCreationOptions }) as PublicKeyCredential if (!credential.response || !(credential.response instanceof AuthenticatorAttestationResponse)) { throw new Error('Invalid credential response') } 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: ErrorResponse = 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 as Error).name === 'NotAllowedError') { errorMessage = 'Registration was cancelled or timed out' } else if ((error as Error).message) { errorMessage = (error as 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() } }) })