/** * Two-Factor Authentication Admin JavaScript * * Handles method selector, AJAX user search, TOTP reset, * notification controls, and TOTP profile setup * * @package Vigilante */ (function($) { 'use strict'; // ========================================================================= // Admin settings module // ========================================================================= var Vigilante2FA = { searchTimeout: null, resetSearchTimeout: null, minSearchLength: 2, init: function() { this.bindEvents(); this.toggleSettingsVisibility(); this.toggleMethodFields(); }, bindEvents: function() { var self = this; $(document).on('change', '#vigilante_2fa_enabled', function() { self.toggleSettingsVisibility(); }); $(document).on('change', 'input[name="login_security[two_factor][method]"]', function() { self.toggleMethodFields(); }); // Exclusion search $(document).on('input', '#vigilante_2fa_user_search', function() { self.handleUserSearch($(this), '.vigilante-2fa-search-results', 'vigilante_search_users_2fa'); }); $(document).on('focus', '#vigilante_2fa_user_search', function() { var $r = $(this).siblings('.vigilante-2fa-search-results'); if ($r.children().length > 0) $r.addClass('active'); }); $(document).on('click', '.vigilante-2fa-search-results .search-result-item', function() { self.addExcludedUser($(this)); }); $(document).on('click', '.vigilante-2fa-excluded-user .remove-user', function() { $(this).closest('.vigilante-2fa-excluded-user').remove(); self.toggleResetButton(); }); // Close dropdowns on outside click $(document).on('click', function(e) { if (!$(e.target).closest('.vigilante-2fa-user-search').length) { $('.vigilante-2fa-search-results, .vigilante-totp-reset-results').removeClass('active'); } }); // Notification $(document).on('click', '#vigilante_2fa_send_notification', function(e) { e.preventDefault(); self.sendNotification($(this)); }); // TOTP reset search $(document).on('input', '#vigilante_totp_reset_search', function() { self.handleResetSearch($(this)); }); $(document).on('focus', '#vigilante_totp_reset_search', function() { var $r = $(this).siblings('.vigilante-totp-reset-results'); if ($r.children().length > 0) $r.addClass('active'); }); $(document).on('click', '.vigilante-totp-reset-results .search-result-item', function() { self.addResetUser($(this)); }); $(document).on('click', '.vigilante-totp-reset-selected .remove-user', function() { $(this).closest('.vigilante-totp-reset-user').remove(); self.toggleResetButton(); }); $(document).on('click', '#vigilante_totp_reset_btn', function(e) { e.preventDefault(); self.resetTotpUsers($(this)); }); }, toggleSettingsVisibility: function() { var enabled = $('#vigilante_2fa_enabled').is(':checked'); $('.vigilante-2fa-settings-wrapper').toggleClass('vigilante-2fa-settings-disabled', !enabled); }, toggleMethodFields: function() { var method = $('input[name="login_security[two_factor][method]"]:checked').val() || 'email'; $('.vigilante-2fa-method-option').removeClass('selected'); $('input[name="login_security[two_factor][method]"]:checked').closest('.vigilante-2fa-method-option').addClass('selected'); $('.vigilante-2fa-totp-only').toggle('totp' === method); $('.vigilante-2fa-email-only').toggle('email' === method); }, handleUserSearch: function($input, resultsSelector, action) { var self = this; var query = $input.val().trim(); var $results = $input.siblings(resultsSelector || '.vigilante-2fa-search-results'); if (this.searchTimeout) clearTimeout(this.searchTimeout); if (query.length < this.minSearchLength) { $results.removeClass('active').empty(); return; } $results.addClass('active').html('
' + self.str('searching') + '
'); this.searchTimeout = setTimeout(function() { $.post(self.ajaxUrl(), { action: action || 'vigilante_search_users_2fa', nonce: self.nonce(), query: query, exclude: self.getExcludedUserIds() }, function(response) { if (response.success && response.data.length > 0) { self.renderResults(response.data, $results); } else { $results.html('
' + self.str('noUsersFound') + '
'); } }); }, 300); }, handleResetSearch: function($input) { var self = this; var query = $input.val().trim(); var $results = $input.siblings('.vigilante-totp-reset-results'); if (this.resetSearchTimeout) clearTimeout(this.resetSearchTimeout); if (query.length < this.minSearchLength) { $results.removeClass('active').empty(); return; } $results.addClass('active').html('
' + self.str('searching') + '
'); this.resetSearchTimeout = setTimeout(function() { $.post(self.ajaxUrl(), { action: 'vigilante_search_totp_users', nonce: self.nonce(), query: query }, function(response) { if (response.success && response.data.length > 0) { self.renderResults(response.data, $results); } else { $results.html('
No TOTP users found
'); } }); }, 300); }, renderResults: function(users, $results) { var self = this; var html = ''; $.each(users, function(i, u) { html += '
'; html += ''; html += '
' + self.esc(u.display_name) + '
'; html += '
' + self.esc(u.user_email) + '
'; }); $results.html(html); }, addExcludedUser: function($item) { var uid = $item.data('user-id'); if ($('.vigilante-2fa-excluded-user[data-user-id="' + uid + '"]').length) return; $('.vigilante-2fa-excluded-users').append( '
' + '' + this.esc($item.data('user-display')) + ' (' + this.esc($item.data('user-email')) + ')' + '' + '' + '
' ); $('#vigilante_2fa_user_search').val(''); $('.vigilante-2fa-search-results').removeClass('active').empty(); }, addResetUser: function($item) { var uid = $item.data('user-id'); if ($('.vigilante-totp-reset-user[data-user-id="' + uid + '"]').length) return; $('.vigilante-totp-reset-selected').append( '
' + '' + this.esc($item.data('user-display')) + ' (' + this.esc($item.data('user-email')) + ')' + '
' ); $('#vigilante_totp_reset_search').val(''); $('.vigilante-totp-reset-results').removeClass('active').empty(); this.toggleResetButton(); }, getExcludedUserIds: function() { var ids = []; $('.vigilante-2fa-excluded-user').each(function() { ids.push($(this).data('user-id')); }); return ids; }, toggleResetButton: function() { $('#vigilante_totp_reset_btn').toggle($('.vigilante-totp-reset-user').length > 0); }, resetTotpUsers: function($btn) { var self = this; var ids = []; $('.vigilante-totp-reset-user').each(function() { ids.push($(this).data('user-id')); }); if (!ids.length) return; $btn.prop('disabled', true).text('Resetting...'); var $status = $('.vigilante-totp-reset-status'); $.post(self.ajaxUrl(), { action: 'vigilante_reset_totp_users', nonce: self.nonce(), user_ids: ids }, function(response) { $btn.prop('disabled', false).text('Reset selected'); if (response.success) { $status.addClass('success').text(response.data.message); $('.vigilante-totp-reset-selected').empty(); self.toggleResetButton(); } else { $status.addClass('error').text(response.data || 'Error'); } setTimeout(function() { $status.removeClass('success error').text(''); }, 4000); }); }, sendNotification: function($btn) { var self = this; var $status = $btn.siblings('.vigilante-2fa-notification-status'); var mode = $('input[name="vigilante_2fa_notify_mode"]:checked').val() || 'all'; $btn.prop('disabled', true).text(self.str('sending')); $status.removeClass('success error').text(''); $.post(self.ajaxUrl(), { action: 'vigilante_send_2fa_notification', nonce: self.nonce(), mode: mode }, function(response) { $btn.prop('disabled', false).text(self.str('sendNotification')); if (response.success) { var msg = response.data.sent + ' ' + self.str('notificationsSent'); if (response.data.skipped > 0) msg += ', ' + response.data.skipped + ' ' + self.str('skipped'); if (response.data.failed > 0) msg += ', ' + response.data.failed + ' ' + self.str('failed'); $status.addClass('success').text(msg); } else { $status.addClass('error').text(response.data || 'Error'); } }); }, // Helpers ajaxUrl: function() { return typeof vigilanteAdmin !== 'undefined' ? vigilanteAdmin.ajaxUrl : ajaxurl; }, nonce: function() { return typeof vigilanteAdmin !== 'undefined' ? vigilanteAdmin.nonce : ''; }, str: function(key) { if (typeof vigilanteAdmin !== 'undefined' && vigilanteAdmin.strings && vigilanteAdmin.strings[key]) return vigilanteAdmin.strings[key]; var fallback = { searching: 'Searching...', noUsersFound: 'No users found', sending: 'Sending...', sendNotification: 'Send notification now', notificationsSent: 'sent', skipped: 'skipped', failed: 'failed' }; return fallback[key] || key; }, esc: function(text) { var map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return String(text).replace(/[&<>"']/g, function(m) { return map[m]; }); } }; // ========================================================================= // TOTP profile setup module (user profile page) // ========================================================================= var VigilanteTOTPProfile = { init: function() { if (!$('.vigilante-totp-profile').length) return; this.bindEvents(); }, bindEvents: function() { var self = this; $(document).on('click', '.vigilante-totp-start-setup', function() { self.startSetup(); }); $(document).on('click', '.vigilante-totp-confirm-setup', function() { self.confirmSetup(); }); $(document).on('click', '.vigilante-totp-regenerate-backup', function() { self.regenerateBackup($(this)); }); $(document).on('click', '.vigilante-totp-reconfigure', function() { self.reconfigure($(this)); }); $(document).on('keypress', '#vigilante_totp_verify_code', function(e) { if (e.which === 13) { e.preventDefault(); $('.vigilante-totp-confirm-setup').click(); } }); }, cfg: function() { return typeof vigilanteTOTP !== 'undefined' ? vigilanteTOTP : { ajaxUrl: ajaxurl, nonce: '', strings: {} }; }, getUserId: function() { // Our own hidden field (most reliable) var uid = $('.vigilante-totp-user-id').val(); if (uid && parseInt(uid, 10) > 0) return parseInt(uid, 10); // WordPress profile hidden fields uid = $('input[name="checkuser_id"]').val() || $('input[name="user_id"]').val(); if (uid && parseInt(uid, 10) > 0) return parseInt(uid, 10); // URL param (edit other user) var m = window.location.search.match(/user_id=(\d+)/); if (m) return parseInt(m[1], 10); // Fallback: server will use current user return 0; }, startSetup: function() { var self = this; var c = this.cfg(); var $btn = $('.vigilante-totp-start-setup'); $btn.prop('disabled', true).text(c.strings.generating || 'Generating...'); $.post(c.ajaxUrl, { action: 'vigilante_totp_get_setup', nonce: c.nonce, user_id: self.getUserId() }, function(response) { if (response.success) { var d = response.data; $('.vigilante-totp-setup-loading').hide(); // Generate QR code client-side var $qrContainer = $('.vigilante-totp-qr-container'); $qrContainer.empty(); var qrEl = document.createElement('div'); $qrContainer.append(qrEl); if (typeof QRCode !== 'undefined') { new QRCode(qrEl, { text: d.uri, width: 200, height: 200, colorDark: '#000000', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.M }); } else { $qrContainer.html('

QR library not loaded. Use the manual key below.

'); } // Format secret in groups of 4 for readability var formattedSecret = d.secret.replace(/(.{4})/g, '$1 ').trim(); $('.vigilante-totp-secret-display').text(formattedSecret); $('.vigilante-totp-setup-qr').data('secret', d.secret).show(); $('#vigilante_totp_verify_code').focus(); } else { $btn.prop('disabled', false).text('Start setup'); alert(response.data || 'Error'); } }); }, confirmSetup: function() { var self = this; var c = this.cfg(); var code = $('#vigilante_totp_verify_code').val().trim(); var secret = $('.vigilante-totp-setup-qr').data('secret'); var $status = $('.vigilante-totp-setup-status'); var $btn = $('.vigilante-totp-confirm-setup'); if (!code || code.length !== 6) { $status.text('Enter a valid 6-digit code.').addClass('error'); return; } $btn.prop('disabled', true); $status.text(c.strings.verifying || 'Verifying...').removeClass('error success'); $.post(c.ajaxUrl, { action: 'vigilante_totp_verify_setup', nonce: c.nonce, user_id: self.getUserId(), code: code, secret: secret }, function(response) { if (response.success) { $status.text('').removeClass('error'); $('.vigilante-totp-setup-qr').hide(); var $success = $('.vigilante-totp-setup-success'); if (response.data.backup_codes) { self.renderCodes(response.data.backup_codes, $success.find('.vigilante-totp-backup-codes-display')); } $success.show(); } else { $btn.prop('disabled', false); $status.text(response.data || 'Invalid code.').addClass('error'); } }); }, regenerateBackup: function($btn) { var self = this; var c = this.cfg(); if (!confirm(c.strings.confirmRegen || 'This will invalidate all existing backup codes. Continue?')) return; $btn.prop('disabled', true); $.post(c.ajaxUrl, { action: 'vigilante_totp_regenerate_backup', nonce: c.nonce, user_id: $btn.data('user') }, function(response) { $btn.prop('disabled', false); if (response.success && response.data.backup_codes) { var $container = $btn.closest('td').find('.vigilante-totp-backup-codes-display'); self.renderCodes(response.data.backup_codes, $container); } }); }, reconfigure: function($btn) { var c = this.cfg(); if (!confirm(c.strings.confirmReconfig || 'This will reset your current TOTP setup. Continue?')) return; $btn.prop('disabled', true).text('Resetting...'); $.post(c.ajaxUrl, { action: 'vigilante_totp_reconfigure', nonce: c.nonce, user_id: $btn.data('user') }, function(response) { if (response.success) { window.location.reload(); } else { $btn.prop('disabled', false).text($btn.data('original-text') || 'Set up new authenticator'); alert(response.data || 'Error'); } }).fail(function() { $btn.prop('disabled', false); }); }, renderCodes: function(codes, $container) { var c = this.cfg(); var html = '
'; html += '

'; html += (c.strings.saveBackupCodes || 'Save these backup codes now. They will not be shown again.'); html += '

'; $.each(codes, function(i, code) { html += '' + (i + 1) + '. ' + code + ''; }); html += '
'; $container.html(html).show(); } }; // ========================================================================= // Password expiration: per-user exclusion search // Same UX as the 2FA exclusion picker, reusing the existing CSS classes. // ========================================================================= var VigilantePwExpExclusion = { searchTimeout: null, minSearchLength: 2, init: function() { if (!$('#vigilante_pwexp_user_search').length) return; this.bindEvents(); }, bindEvents: function() { var self = this; $(document).on('input', '#vigilante_pwexp_user_search', function() { self.handleSearch($(this)); }); $(document).on('focus', '#vigilante_pwexp_user_search', function() { var $r = $(this).siblings('.vigilante-pwexp-search-results'); if ($r.children().length > 0) $r.addClass('active'); }); $(document).on('click', '.vigilante-pwexp-search-results .search-result-item', function() { self.addExcludedUser($(this)); }); $(document).on('click', '.vigilante-pwexp-excluded-user .remove-user', function() { $(this).closest('.vigilante-pwexp-excluded-user').remove(); }); $(document).on('click', function(e) { if (!$(e.target).closest('#vigilante_pwexp_user_search').length && !$(e.target).closest('.vigilante-pwexp-search-results').length) { $('.vigilante-pwexp-search-results').removeClass('active'); } }); }, handleSearch: function($input) { var self = this; var query = $input.val().trim(); var $results = $input.siblings('.vigilante-pwexp-search-results'); if (this.searchTimeout) clearTimeout(this.searchTimeout); if (query.length < this.minSearchLength) { $results.removeClass('active').empty(); return; } $results.addClass('active').html('
' + Vigilante2FA.str('searching') + '
'); this.searchTimeout = setTimeout(function() { $.post(Vigilante2FA.ajaxUrl(), { action: 'vigilante_search_users_password_reset', nonce: Vigilante2FA.nonce(), query: query }, function(response) { if (response.success && response.data.length > 0) { Vigilante2FA.renderResults(response.data, $results); } else { $results.html('
' + Vigilante2FA.str('noUsersFound') + '
'); } }); }, 300); }, addExcludedUser: function($item) { var uid = $item.data('user-id'); if ($('.vigilante-pwexp-excluded-user[data-user-id="' + uid + '"]').length) return; $('.vigilante-pwexp-excluded-users').append( '
' + '' + Vigilante2FA.esc($item.data('user-display')) + ' (' + Vigilante2FA.esc($item.data('user-email')) + ')' + '' + '' + '
' ); $('#vigilante_pwexp_user_search').val(''); $('.vigilante-pwexp-search-results').removeClass('active').empty(); } }; $(document).ready(function() { Vigilante2FA.init(); VigilanteTOTPProfile.init(); VigilantePwExpExclusion.init(); }); })(jQuery);