towerops/assets/js/passkey_settings.js
2026-01-09 12:26:32 -06:00

170 lines
5 KiB
JavaScript

// 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()
}
})
})