passkeys
1 day ago
profile
1 day ago
wp2fa-dialog.js
1 day ago
wp2fa-premium-badge-dialog.js
1 day ago
wp2fa-wizard-backup-codes.js
1 day ago
wp2fa-wizard-email.js
1 day ago
wp2fa-wizard-totp.js
1 day ago
wp2fa-wizard.js
1 day ago
wp2fa-wizard-backup-codes.js
284 lines
| 1 | /** |
| 2 | * WP 2FA - Backup Codes Method Module |
| 3 | * |
| 4 | * Registers backup codes as a secondary/backup method with the wizard via wp.hooks. |
| 5 | * After a primary method is validated, the wizard core calls the backup methods flow. |
| 6 | * This module renders a "generate backup codes" step with download/copy/print options. |
| 7 | * |
| 8 | * @package wp2fa |
| 9 | * @since 4.0.0 |
| 10 | * |
| 11 | * Hooks this module uses: |
| 12 | * - wp2fa_wizard_backup_methods (filter) — registers backup codes as available |
| 13 | * - wp2fa_wizard_render_backup_method (action) — renders backup codes generation UI |
| 14 | */ |
| 15 | ( function () { |
| 16 | 'use strict'; |
| 17 | |
| 18 | if ( ! window.wp || ! window.wp.hooks ) { |
| 19 | return; |
| 20 | } |
| 21 | |
| 22 | var hooks = wp.hooks; |
| 23 | var METHOD_ID = 'backup_codes'; |
| 24 | |
| 25 | /* ------------------------------------------------------- |
| 26 | * Register as a backup method |
| 27 | * ------------------------------------------------------- */ |
| 28 | hooks.addFilter( 'wp2fa_wizard_backup_methods', 'wp2fa/backup-codes', function ( methods, context ) { |
| 29 | if ( typeof wp2faWizardData === 'undefined' ) { |
| 30 | return methods; |
| 31 | } |
| 32 | var backupMethods = wp2faWizardData.backupMethods || {}; |
| 33 | if ( ! backupMethods[ METHOD_ID ] ) { |
| 34 | return methods; |
| 35 | } |
| 36 | |
| 37 | methods.push( { |
| 38 | id: METHOD_ID, |
| 39 | name: wp2faWizardData.backupMethodLabels && wp2faWizardData.backupMethodLabels[ METHOD_ID ] |
| 40 | ? wp2faWizardData.backupMethodLabels[ METHOD_ID ] |
| 41 | : 'Backup Codes', |
| 42 | description: '', |
| 43 | order: 1 |
| 44 | } ); |
| 45 | |
| 46 | return methods; |
| 47 | } ); |
| 48 | |
| 49 | /* ------------------------------------------------------- |
| 50 | * Render backup codes generation step |
| 51 | * ------------------------------------------------------- */ |
| 52 | hooks.addAction( 'wp2fa_wizard_render_backup_method', 'wp2fa/backup-codes', function ( methodId, container, context ) { |
| 53 | if ( methodId !== METHOD_ID ) { |
| 54 | return; |
| 55 | } |
| 56 | |
| 57 | var backupData = wp2faWizardData.backupCodesData || {}; |
| 58 | |
| 59 | renderGenerateStep(); |
| 60 | |
| 61 | function renderGenerateStep() { |
| 62 | container.innerHTML = ''; |
| 63 | |
| 64 | /* Title — hide when user already selected this from a multi-method list */ |
| 65 | var titleEl = document.getElementById( 'wp2fa-wizard-title' ); |
| 66 | if ( titleEl ) { |
| 67 | if ( Object.keys( wp2faWizardData.backupMethods || {} ).length > 1 ) { |
| 68 | titleEl.innerHTML = ''; |
| 69 | } else { |
| 70 | titleEl.innerHTML = hooks.applyFilters( |
| 71 | 'wp2fa_wizard_step_title', |
| 72 | wp2faWizardData.i18n.backupCodesTitle || '', |
| 73 | 'backup-method-backup_codes' |
| 74 | ); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | /* Render body via template */ |
| 79 | var generateTmpl = wp.template( 'wp2fa-backup-codes-generate' ); |
| 80 | container.innerHTML = generateTmpl( { |
| 81 | intro: wp2faWizardData.i18n.backupCodesIntro || 'Backup codes let you log in when you can\'t access your primary 2FA method. Each code can only be used once. We recommend generating and safely storing your backup codes now.', |
| 82 | generateLabel: wp2faWizardData.i18n.generateBackupCodes || 'Generate list of backup codes' |
| 83 | } ); |
| 84 | |
| 85 | /* Generate button event */ |
| 86 | var generateBtn = container.querySelector( '#wp2fa-backup-codes-generate-btn' ); |
| 87 | generateBtn.addEventListener( 'click', function () { |
| 88 | generateBtn.disabled = true; |
| 89 | var spinner = window.wp2faWizard.utils.addSpinner( generateBtn ); |
| 90 | |
| 91 | var formData = new FormData(); |
| 92 | formData.append( 'action', 'wp2fa_run_ajax_generate_json' ); |
| 93 | formData.append( 'nonce', backupData.nonce || '' ); |
| 94 | |
| 95 | fetch( wp2faWizardData.ajaxUrl, { |
| 96 | method: 'POST', |
| 97 | credentials: 'same-origin', |
| 98 | body: formData |
| 99 | } ) |
| 100 | .then( function ( res ) { return res.json(); } ) |
| 101 | .then( function ( data ) { |
| 102 | generateBtn.disabled = false; |
| 103 | window.wp2faWizard.utils.removeSpinner( spinner ); |
| 104 | if ( data.success && data.data && data.data.codes ) { |
| 105 | renderCodesDisplay( data.data.codes ); |
| 106 | } else { |
| 107 | var errEl = document.createElement( 'div' ); |
| 108 | errEl.className = 'wp2fa-wizard-verification-response wp2fa-response-error'; |
| 109 | errEl.textContent = wp2faWizardData.i18n.backupCodesError || 'Failed to generate backup codes.'; |
| 110 | container.appendChild( errEl ); |
| 111 | setTimeout( function () { |
| 112 | errEl.textContent = ''; |
| 113 | errEl.className = 'wp2fa-wizard-verification-response'; |
| 114 | }, 3000 ); |
| 115 | } |
| 116 | } ) |
| 117 | .catch( function () { |
| 118 | generateBtn.disabled = false; |
| 119 | window.wp2faWizard.utils.removeSpinner( spinner ); |
| 120 | } ); |
| 121 | } ); |
| 122 | } |
| 123 | |
| 124 | function renderCodesDisplay( codes ) { |
| 125 | container.innerHTML = ''; |
| 126 | |
| 127 | /* Title */ |
| 128 | var titleEl = document.getElementById( 'wp2fa-wizard-title' ); |
| 129 | if ( titleEl ) { |
| 130 | titleEl.innerHTML = hooks.applyFilters( |
| 131 | 'wp2fa_wizard_step_title', |
| 132 | wp2faWizardData.i18n.yourBackupCodes || 'Your backup codes', |
| 133 | 'backup-method-backup_codes-display' |
| 134 | ); |
| 135 | } |
| 136 | |
| 137 | var codesLines = []; |
| 138 | for ( var ci = 0; ci < codes.length; ci++ ) { |
| 139 | codesLines.push( ( ci + 1 ) + ': ' + codes[ ci ] ); |
| 140 | } |
| 141 | var codesText = codesLines.join( '\n' ); |
| 142 | |
| 143 | /* Render body via template */ |
| 144 | var displayTmpl = wp.template( 'wp2fa-backup-codes-display' ); |
| 145 | container.innerHTML = displayTmpl( { |
| 146 | intro: wp2faWizardData.i18n.backupCodesGenerated || 'Here are your backup codes. Store them somewhere safe. Each code can only be used once.', |
| 147 | codesText: codesText, |
| 148 | codesCount: Math.min( codes.length, 10 ), |
| 149 | showCopyBtn: !! ( navigator.clipboard || document.queryCommandSupported( 'copy' ) ), |
| 150 | copyLabel: wp2faWizardData.i18n.copyBackupCodes || 'Copy', |
| 151 | downloadLabel: wp2faWizardData.i18n.downloadBackupCodes || 'Download', |
| 152 | printLabel: wp2faWizardData.i18n.printBackupCodes || 'Print', |
| 153 | emailLabel: wp2faWizardData.i18n.sendBackupCodesEmail || 'Send via email', |
| 154 | emailNonce: backupData.emailNonce || '' |
| 155 | } ); |
| 156 | |
| 157 | var textarea = container.querySelector( '#wp2fa-backup-codes-textarea' ); |
| 158 | |
| 159 | /* Copy button event */ |
| 160 | var copyBtn = container.querySelector( '#wp2fa-backup-codes-copy-btn' ); |
| 161 | if ( copyBtn ) { |
| 162 | copyBtn.addEventListener( 'click', function () { |
| 163 | if ( navigator.clipboard && window.isSecureContext ) { |
| 164 | navigator.clipboard.writeText( codesText ); |
| 165 | } else { |
| 166 | textarea.select(); |
| 167 | document.execCommand( 'copy' ); |
| 168 | } |
| 169 | copyBtn.textContent = wp2faWizardData.i18n.copied || 'Copied!'; |
| 170 | setTimeout( function () { |
| 171 | copyBtn.textContent = wp2faWizardData.i18n.copyBackupCodes || 'Copy'; |
| 172 | }, 2000 ); |
| 173 | } ); |
| 174 | } |
| 175 | |
| 176 | /* Download button event */ |
| 177 | container.querySelector( '#wp2fa-backup-codes-download-btn' ).addEventListener( 'click', function () { |
| 178 | var siteUrl = window.location.hostname || 'site'; |
| 179 | var blob = new Blob( [ 'Backup codes for ' + siteUrl + '\n\n' + codesText + '\n' ], { type: 'text/plain' } ); |
| 180 | var url = URL.createObjectURL( blob ); |
| 181 | var a = document.createElement( 'a' ); |
| 182 | a.href = url; |
| 183 | a.download = 'wp-2fa-backup-codes.txt'; |
| 184 | document.body.appendChild( a ); |
| 185 | a.click(); |
| 186 | document.body.removeChild( a ); |
| 187 | URL.revokeObjectURL( url ); |
| 188 | } ); |
| 189 | |
| 190 | /* Print button event */ |
| 191 | container.querySelector( '#wp2fa-backup-codes-print-btn' ).addEventListener( 'click', function () { |
| 192 | var printWindow = window.open( '', '_blank' ); |
| 193 | if ( printWindow ) { |
| 194 | var safeText = codesText.replace( /&/g, '&' ).replace( /</g, '<' ).replace( />/g, '>' ).replace( /"/g, '"' ); |
| 195 | printWindow.document.write( '<html><head><title>Backup Codes</title></head><body>' ); |
| 196 | printWindow.document.write( '<h1>Backup Codes</h1>' ); |
| 197 | printWindow.document.write( '<pre>' + safeText + '</pre>' ); |
| 198 | printWindow.document.write( '</body></html>' ); |
| 199 | printWindow.document.close(); |
| 200 | printWindow.print(); |
| 201 | } |
| 202 | } ); |
| 203 | |
| 204 | /* Email button event */ |
| 205 | var emailBtn = container.querySelector( '#wp2fa-backup-codes-email-btn' ); |
| 206 | if ( emailBtn ) { |
| 207 | emailBtn.addEventListener( 'click', function () { |
| 208 | sendCodesViaEmail( emailBtn, codesText ); |
| 209 | } ); |
| 210 | } |
| 211 | |
| 212 | /* Footer via template */ |
| 213 | var footer = document.getElementById( 'wp2fa-wizard-footer' ); |
| 214 | var displayFooterTmpl = wp.template( 'wp2fa-backup-codes-display-footer' ); |
| 215 | footer.innerHTML = displayFooterTmpl( { |
| 216 | closeLabel: wp2faWizardData.i18n.imReadyClose || "I'm ready, close the wizard" |
| 217 | } ); |
| 218 | |
| 219 | footer.querySelector( '#wp2fa-backup-codes-close-btn' ).addEventListener( 'click', function () { |
| 220 | window.wp2faWizard.nextBackupMethod(); |
| 221 | } ); |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Send codes via email. |
| 226 | * |
| 227 | * @param {HTMLElement} emailBtn The email button element. |
| 228 | * @param {string} codesText The formatted codes text to send. |
| 229 | */ |
| 230 | function sendCodesViaEmail( emailBtn, codesText ) { |
| 231 | var nonce = emailBtn.getAttribute( 'data-nonce' ); |
| 232 | var ajaxUrl = wp2faWizardData.ajaxUrl; |
| 233 | |
| 234 | if ( ! nonce ) { |
| 235 | showStatus( 'Invalid request. Please try again.', true ); |
| 236 | return; |
| 237 | } |
| 238 | |
| 239 | emailBtn.disabled = true; |
| 240 | var originalText = emailBtn.textContent; |
| 241 | |
| 242 | var formData = new FormData(); |
| 243 | formData.append( 'action', 'send_backup_codes_email' ); |
| 244 | formData.append( '_wpnonce', nonce ); |
| 245 | formData.append( 'codes', JSON.stringify( codesText ) ); |
| 246 | |
| 247 | fetch( ajaxUrl, { |
| 248 | method: 'POST', |
| 249 | credentials: 'same-origin', |
| 250 | body: formData |
| 251 | } ) |
| 252 | .then( function ( res ) { return res.json(); } ) |
| 253 | .then( function ( data ) { |
| 254 | emailBtn.disabled = false; |
| 255 | if ( data.success ) { |
| 256 | showStatus( wp2faWizardData.i18n.backupCodesEmailSent || 'Codes sent to your email.', false ); |
| 257 | } else { |
| 258 | showStatus( wp2faWizardData.i18n.backupCodesEmailError || 'Failed to send email.', true ); |
| 259 | } |
| 260 | } ) |
| 261 | .catch( function () { |
| 262 | emailBtn.disabled = false; |
| 263 | showStatus( wp2faWizardData.i18n.networkError || 'Network error. Please try again.', true ); |
| 264 | } ); |
| 265 | } |
| 266 | |
| 267 | /** |
| 268 | * Show status message below the buttons. |
| 269 | * |
| 270 | * @param {string} message The status message. |
| 271 | * @param {boolean} isError Whether this is an error message. |
| 272 | */ |
| 273 | function showStatus( message, isError ) { |
| 274 | var el = container.querySelector( '#wp2fa-backup-codes-status' ); |
| 275 | if ( el ) { |
| 276 | el.textContent = message; |
| 277 | el.style.display = ''; |
| 278 | el.style.color = isError ? '#d63638' : '#00a32a'; |
| 279 | el.style.marginTop = '10px'; |
| 280 | } |
| 281 | } |
| 282 | } ); |
| 283 | }() ); |
| 284 |