Jake Vanderwerf
2026-01-01 bc379ff1046696781c0c251e192e11646cb27e28
inc/managers/LoginManager.php
@@ -17,10 +17,7 @@
class LoginManager
{
   protected Features $siteFeatures;
   protected ?MagicLinkManager $magicLink = null;
   protected ?MetaForm $metaForm = null;
   protected EmailManager $emailManager;
   protected AjaxRateLimiter $rateLimiter;
   protected CacheManager $cache;
@@ -44,8 +41,7 @@
   public function __construct()
   {
      $this->siteFeatures = Features::forSite();
      $this->emailManager = new EmailManager();
      $this->rateLimiter = new AjaxRateLimiter();
      $this->cache = CacheManager::for('login');
@@ -67,10 +63,18 @@
      // Login success handling
      add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
      add_filter( 'login_url', [$this, 'loginUrl'], 10, 3 );
      // Allow other features to register handlers
      do_action('jvbLoginManagerInit', $this);
      add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
      add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1);
   }
   public function excludeLoginSitemap(array $ids): array
   {
      $ids[] = $this->getLoginPage();
      return $ids;
   }
   /**************************************************************************
      * SETUP & CONFIGURATION
   **************************************************************************/
@@ -90,7 +94,7 @@
         return;
      }
      // Build custom login URL with all query args
      $custom_login_page = home_url('/login');
      $custom_login_page = home_url('/login/');
      $query_args = $_GET;
      // Remove WordPress internal args
@@ -287,6 +291,18 @@
         }
      }
   }
   public function loginUrl(string $login_url, string $redirect, bool $force_reauth):string
   {
      // This will append /custom-login/ to you main site URL as configured in general settings (ie https://domain.com/custom-login/)
      $login_url = site_url( '/login/', 'login' );
      if ( ! empty( $redirect ) ) {
         $login_url = add_query_arg( 'redirect_to', urlencode( $redirect ), $login_url );
      }
      if ( $force_reauth ) {
         $login_url = add_query_arg( 'reauth', '1', $login_url );
      }
      return $login_url;
   }
   public function getLoginPage():int|false
   {
      return (int)get_option(BASE.'login_page');
@@ -308,7 +324,6 @@
      if (!Features::forSite()->has('magicLink')) {
         return;
      }
      $this->magicLink = new MagicLinkManager();
   }
   /*********************************************************************
@@ -597,7 +612,8 @@
            $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
            $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
            echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
               <span class="screen-reader-text">Toggle dark mode</span>
                    <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'.
               jvbIcon('sun-dim', ['title'=> 'Light Mode']).
               jvbIcon('moon', ['title'=>'Dark Mode']).
               '</span></label>';
@@ -664,10 +680,7 @@
   /*************************************************************************
   *  SECURITY & VALIDATION
   *************************************************************************/
   protected function checkAjaxRateLimit(string $action): bool
   {
      return $this->rateLimiter->checkLimit($action);
   }
   protected function checkRequestId(): bool
   {
      $request_id = $_POST['request_id'] ?? '';
@@ -815,7 +828,7 @@
   protected function maybeMagicLink(): void
   {
      if (!$this->magicLink || !in_array($this->action, ['login', 'lostpassword'])) {
      if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) {
         return;
      }
      ?>
@@ -841,8 +854,7 @@
      $action = $this->getAction();
      ob_start();
      ?>
      <script type="text/javascript">
      window.checkedEmails = new Set();
      document.addEventListener('DOMContentLoaded', () => {
         const form = document.querySelector('.login form');
         if (!form) return;
@@ -863,33 +875,34 @@
         window.LoginController.subscribe((event, data) => {
            if (event === 'form-submit') {
               handleFormSubmission(data);
            } else if (event === 'field-validated' && data.name === 'user_email') {
               if (!window.checkedEmails.has(data.value)){
                  checkEmail(data.value, data);
               }
            }
         });
         async function handleFormSubmission(data) {
            const { formId, config, data: formData } = data;
            const form = config.element;
            const submit = form.querySelector('[type=submit]');
            let oldText = submit.textContent;
            // Show uploading status
            window.LoginController.showFormStatus(formId, 'uploading');
         let realFormData = data.fullData;
         const { formId, config, data: formData } = data;
            try {
               submit.disabled = true;
               submit.textContent = 'Loading...';
               const response = await fetch(`${jvbSettings.api}<?=($action === 'magic') ? $action : 'auth/'.$action?>`, {
                  method: 'POST',
                  headers: {
                     'Content-Type': 'application/json',
                     'X-WP-Nonce': jvbSettings.nonce
                  },
                  body: JSON.stringify(formData)
               });
         const form = config.element;
         const submit = form.querySelector('[type=submit]');
         let oldText = submit.textContent;
         window.LoginController.showFormStatus(formId, 'uploading');
         try {
            submit.disabled = true;
            submit.textContent = 'Loading...';
            const response = await fetch(`${jvbSettings.api}<?=($action === 'magic') ? $action : 'auth/'.$action?>`, {
               method: 'POST',
               headers: {
                     'Content-Type': 'application/json',
                  'X-WP-Nonce': window.auth.getNonce()
               },
               body: JSON.stringify(realFormData)
            });
               const result = await response.json();
@@ -908,11 +921,16 @@
                  window.LoginController.handleFormSuccess(form, result);
               }
               if (window.auth && typeof window.auth.handleLogin === 'function' && Object.hasOwn(result, 'auth')) {
                  console.log('Awaiting Auth...');
                  await window.auth.handleLogin(result.auth); // Pass the full result
               }
               // Handle redirect
               if (result.redirect) {
                  setTimeout(() => {
                     window.location.href = result.redirect;
                  }, 500); // Brief delay to show success message
                  }, 200); // Brief delay to show success message
               }
            } catch (error) {
@@ -927,68 +945,11 @@
               submit.disabled = false;
            }
         }
         async function checkEmail(email, input) {
            window.checkedEmails.add(email);
            let wrapper = input.closest('.field');
            let submit = input.closest('form').querySelector('[type=submit]');
            try {
               submit.disabled = true;
               window.LoginController.showSuccess(wrapper, 'Checking our records...');
               const response = await fetch(`${jvbSettings.api}auth/email`, {
                  method: 'POST',
                  headers: {
                     'Content-Type': 'application/json',
                     'X-WP-Nonce': jvbSettings.nonce
                  },
                  body: JSON.stringify({ email: email })
               });
               const result = await response.json();
               if (!response.ok) {
                  return; // On error, allow to proceed (fail open)
               }
               if (result.exists) {
                  <?php
                  switch ($action) {
                     case 'register':
                        echo 'window.LoginController.showError(wrapper,\'This email is already registered. Log in instead?\');';
                        break;
                     case 'login':
                     case 'lostpassword':
                     case 'magic':
                        echo 'window.LoginController.showSuccess(wrapper,\'Email exists in our system.\');';
                        break;
                  }
                  ?>
               } else {
                  <?php
                  switch ($action) {
                     case 'register':
                        echo 'window.LoginController.showSuccess(wrapper,\'Email is available!\');';
                        break;
                     case 'login':
                     case 'lostpassword':
                     case 'magic':
                        echo 'window.LoginController.showError(wrapper,\'This email doesn\\\'t seem to exist in our system. Create account instead?\');';
                        break;
                  }
                  ?>
               }
               return true; // Email is available
            } catch (error) {
               console.error('Email check failed:', error);
               return true; // On network error, allow to proceed
            }finally {
               submit.disabled = false;
            }
         }
      });
      </script>
      <?php
      $script = ob_get_clean();
@@ -1022,6 +983,11 @@
      wp_safe_redirect($login_url);
      exit;
   }
   public function saveRegistrationFields(int $user_id, array $userdata):void
   {
   }
}
// Initialize the login manager