1$(document).ready(function() { 2 var helper = { 3 atb: b => { 4 let u = new Uint8Array(b), s = ""; 5 for (let i = 0; i < u.byteLength; i++) { s += String.fromCharCode(u[i]); } 6 return btoa(s); 7 }, 8 9 bta: o => { 10 let pre = "=?BINARY?B?", suf = "?="; 11 for (let k in o) { 12 if (typeof o[k] == "string") { 13 let s = o[k]; 14 if (s.substring(0, pre.length) == pre && s.substring(s.length - suf.length) == suf) { 15 let b = window.atob(s.substring(pre.length, s.length - suf.length)), 16 u = new Uint8Array(b.length); 17 for (let i = 0; i < b.length; i++) { u[i] = b.charCodeAt(i); } 18 o[k] = u.buffer; 19 } 20 } else { helper.bta(o[k]); } 21 } 22 } 23 }; 24 25 $('.btn-log-in-with-passkey').on('click', function (e) { 26 e.preventDefault(); 27 28 $('#myform').find('.alert').remove(); 29 30 var $btn = $(this); 31 32 $btn 33 .css({ 34 width: $btn.outerWidth() 35 }) 36 .html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>') 37 .prop('disabled', true); 38 39 // Request for passkey challenge 40 $.get('/passkey?action=log-in', async function (json) { 41 try { 42 helper.bta(json); 43 const credential = await navigator.credentials.get(json); 44 45 $.post('/passkey', { 46 action: 'validate', 47 id: credential.id, 48 clientDataJSON: helper.atb(credential.response.clientDataJSON), 49 authenticatorData: helper.atb(credential.response.authenticatorData), 50 signature: helper.atb(credential.response.signature), 51 userHandle: helper.atb(credential.response.userHandle), 52 }, function (json) { 53 if (json.status !== 'OK') { 54 $('<div class="alert alert-danger"></div>').text(json.message).insertAfter('#myform > :first-child'); 55 $btn.html('<i class="fa-solid fa-key"></i> Log In With Passkey').prop('disabled', false); 56 57 return; 58 } 59 60 window.location.href = window.location.href; 61 }, 'json').fail(function () { 62 $('<div class="alert alert-danger"></div>').text('Unable to contact server. Please try again.').insertAfter('#myform > :first-child'); 63 $btn.html('<i class="fa-solid fa-key"></i> Log In With Passkey').prop('disabled', false); 64 }); 65 } catch (e) { 66 $('<div class="alert alert-danger">Operation canceled or timed out. Please try again.</div>').insertAfter('#myform > :first-child'); 67 $btn.html('<i class="fa-solid fa-key"></i> Log In With Passkey').prop('disabled', false); 68 } 69 }, 'json').fail(function () { 70 $('<div class="alert alert-danger"></div>').text('Unable to contact server. Please try again.').insertAfter('#myform > :first-child'); 71 $btn.html('<i class="fa-solid fa-key"></i> Log In With Passkey').prop('disabled', false); 72 }); 73 }); 74 75 $('#add-new-passkey').on('click', function (e) { 76 e.preventDefault(); 77 78 $('#modal-passkey').find('.alert').remove(); 79 $('input[name="deviceName"]').val(''); 80 var bsModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('modal-passkey')); 81 bsModal.show(); 82 }); 83 84 $('#btn-add-passkey').on('click', function (e) { 85 e.preventDefault(); 86 87 var $btn = $(this); 88 var deviceName = $('input[name=\"deviceName\"]').val().trim(); 89 90 $btn 91 .css({ 92 width: $btn.outerWidth() 93 }) 94 .html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>') 95 .prop('disabled', true); 96 97 $('#modal-passkey').find('.alert').remove(); 98 99 if (!deviceName.match(/^[A-Za-z0-9_\- ]{1,50}$/)) {
100 $('#modal-passkey .modal-body').append('<div class="alert alert-danger mt-2">Device name must be 1-50 alphanumeric characters.</div>'); 101 $btn.prop('disabled', false).html('Add Passkey'); 102 return; 103 } 104 105 // Create new passkey 106 $.post('/passkey', { 107 action: 'register', 108 deviceName: deviceName, 109 }, async function(json) { 110 if (typeof json.message !== 'undefined') { 111 $('<div class="alert alert-danger mt-2"></div>').text(json.message).appendTo('#modal-passkey .modal-body'); 112 $btn.prop('disabled', false).html('Add Passkey'); 113 return; 114 } 115 116 try { 117 helper.bta(json); 118 const credential = await navigator.credentials.create(json); 119 120 $.post('/passkey', { 121 action: 'save', 122 clientDataJSON: helper.atb(credential.response.clientDataJSON), 123 attestationObject: helper.atb(credential.response.attestationObject), 124 deviceName: deviceName, 125 }, function (json) { 126 if (json.status !== 'OK') { 127 $('<div class="alert alert-danger mt-2"></div>').text(json.message).appendTo('#modal-passkey .modal-body'); 128 $btn.prop('disabled', false).html('Add Passkey'); 129 return; 130 } 131 132 window.location.href = window.location.href; 133 }, 'json').fail(function () { 134 $('#modal-passkey .modal-body').append('<div class="alert alert-danger mt-2">Unable to contact server. Please try again.</div>'); 135 $btn.prop('disabled', false).html('Add Passkey'); 136 }); 137 } catch (e) { 138 errorMessage = ''; 139 if (e.name === 'InvalidStateError') { 140 errorMessage = 'This device is already registered for your account.'; 141 } else if (err.name === 'NotAllowedError') { 142 errorMessage = 'Passkey creation was cancelled or timed out.'; 143 } else { 144 errorMessage = 'Operation canceled or timed out. Please try again.'; 145 } 146 $('<div class="alert alert-danger mt-2"></div>').text(errorMessage).appendTo('#modal-passkey .modal-body'); 147 $btn.prop('disabled', false).html('Add Passkey'); 148 } 149 }, 'json').fail(function () {
150 $('#modal-passkey .modal-body').append('<div class="alert alert-danger mt-2">Unable to contact server. Please try again.</div>'); 151 $btn.prop('disabled', false).html('Add Passkey'); 152 }); 153 }); 154 155 $('.btn-delete-passkey').on('click', function (e) { 156 e.preventDefault(); 157 158 if (confirm('Are you sure you want to revoke access for this passkey?')) { 159 var $form = $('<form>', { method: 'post' }); 160 161 $('<input>', { 162 type: 'hidden', 163 name: 'deletePasskey', 164 value: $(this).data('key') 165 }).appendTo($form); 166 167 $('body').append($form); 168 $form.submit(); 169 } 170 }); 171});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.