Jake Vanderwerf
2026-05-01 48721c85ebcfa973ee81719d2467ca80e4253dc9
inc/managers/LoginManager.php
@@ -1,13 +1,11 @@
<?php
namespace JVBase\managers;
use JVBase\blocks\CustomBlocks;
use JVBase\base\Site;
use JVBase\forms\TaxonomySelector;
use JVBase\meta\MetaManager;
use JVBase\meta\MetaForm;
use JVBase\managers\AjaxRateLimiter;
use JVBase\utility\Features;
use WP_Error;
use JVBase\meta\Form;
use JVBase\registrar\Registrar;use WP_Error;
use WP_User;
if (!defined('ABSPATH')) {
@@ -16,12 +14,8 @@
class LoginManager
{
   protected Features $siteFeatures;
   protected ?MagicLinkManager $magicLink = null;
   protected ?MetaForm $metaForm = null;
   protected EmailManager $emailManager;
   protected AjaxRateLimiter $rateLimiter;
   protected CacheManager $cache;
   protected ?Form $form = null;
   protected Cache $cache;
   protected array $forms =[];
@@ -43,14 +37,10 @@
   public function __construct()
   {
      $this->siteFeatures = Features::forSite();
      $this->emailManager = new EmailManager();
      $this->cache = CacheManager::for('login');
      $this->cache = Cache::for('login');
      // Initialize magic link support if enabled
      if ($this->siteFeatures->has('magicLink')) {
      if (Site::has('magicLink')) {
         $this->initMagicLinkSupport();
      }
@@ -67,10 +57,20 @@
      // Login success handling
      add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
      add_filter('lostpassword_url', [$this, 'resetPasswordUrl'], 10, 2);
      add_filter( 'login_url', [$this, 'loginUrl'], 10, 3 );
      add_filter( 'logout_url', [$this, 'logoutUrl'], 10, 2 );
      // 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 +90,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
@@ -125,7 +125,7 @@
               'placeholder'=> 'look@me.com'
            ]
         ];
         if (Features::forSite()->has('referrals')) {
         if (Site::has('referrals')) {
            $fields['referral_code'] = [
               'type'   => 'text',
               'required'=> false,
@@ -133,19 +133,19 @@
               'hint'   => 'Have a referral code? Paste it here!'
            ];
         }
         if (count(JVB_USER) > 1) {
            foreach (JVB_USER as $slug => $config) {
               if (!array_key_exists('can_register', $config) || !$config['can_register']) {
                  continue;
               }
               $icon = $config['icon'] ?? '';
         $canRegister = Registrar::getFeatured('can_register', 'user');
         if (!empty($canRegister)) {
            foreach ($canRegister as $role) {
               $registrar = Registrar::getInstance($role);
               $config = $registrar->getConfig('register');
               $icon = $registrar->getIcon('user');
               $icon = ($icon !== '') ? jvbIcon($icon) : '';
               $select[$slug] = '<span class="label">'.$icon.$config['label'].'</span><span class="text">'.$config['register']['text']??''.'</span>';
               if (!empty($config['register']['fields']??[])){
                  foreach ($config['register']['fields'] as $field) {
               $select[$role] = '<span class="label">'.$icon.$registrar->getSingular().'</span><span class="text">'.$config['description']??Site::login()->getDescription('register')??''.'</span>';
               if (!empty($config['fields'])){
                  foreach ($config['fields'] as $field) {
                     $field['condition'] = [
                        'field'  => 'user_select',
                        'value'  => $slug,
                        'value'  => $role,
                        'operator'  => '=='
                     ];
                     $fields[] = $field;
@@ -222,12 +222,14 @@
                  'type' => 'email',
                  'label' => __('Email Address', 'jvb'),
                  'required' => true,
                  'autocomplete' => 'email',
                  'placeholder' => 'look@me.com',
               ],
               'user_password' => [
                  'type' => 'text',
                  'subtype'=> 'password',
                  'label' => __('Password', 'jvb'),
                  'autocomplete' => 'current-password',
                  'required' => true,
               ],
               'remember_me' => [
@@ -287,6 +289,40 @@
         }
      }
   }
   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 logoutUrl(string $logout_url, string $redirect): string
   {
      // Build custom logout URL
      $logout_url = site_url('/login/', 'login');
      $logout_url = add_query_arg('action', 'logout', $logout_url);
      if (!empty($redirect)) {
         $logout_url = add_query_arg('redirect_to', urlencode($redirect), $logout_url);
      }
      // Add nonce for security
      return wp_nonce_url($logout_url, 'log-out');
   }
   public function resetPasswordUrl(string $url, string $redirect):string
   {
      error_log('reset Password Url:'.print_r($url, true));
      error_log('reset password redirect: '.print_r($redirect, true));
      return str_replace('wp_login.php', 'login/', $url);
   }
   public function getLoginPage():int|false
   {
      return (int)get_option(BASE.'login_page');
@@ -305,10 +341,9 @@
   protected function initMagicLinkSupport(): void
   {
      if (!Features::forSite()->has('magicLink')) {
      if (!Site::has('magicLink')) {
         return;
      }
      $this->magicLink = new MagicLinkManager();
   }
   /*********************************************************************
@@ -369,17 +404,11 @@
   protected function setup():void
   {
      $this->action = $this->getAction();
      if (in_array($this->action, ['logout']) || array_key_exists('loggedout', $_GET)) {
      if ($this->action == 'logout' || array_key_exists('loggedout', $_GET)) {
         wp_logout();
         wp_redirect(esc_attr($_GET['redirect_to'] ?? get_home_url()));
         exit;
      }
      if (in_array($this->action, ['rp', 'resetpass']) && !is_user_logged_in()) {
         wp_redirect(wp_login_url());
         exit;
      } elseif (is_user_logged_in()) {
         wp_redirect(get_home_url(null, '/dash/'));
      }
      $this->setupLabels();
      $this->setupFields();
      $this->setupTitle();
@@ -406,11 +435,9 @@
   protected function customStyles():void
   {
      $logo = get_theme_mod('custom_logo');
      $small = $large = '';
      if ($logo) {
         $small = wp_get_attachment_image_src($logo, 'medium')[0];
         $large = wp_get_attachment_image_src($logo, 'large')[0];
         $small = wp_get_attachment_image_src($logo, 'medium')[0]??'';
         $large = wp_get_attachment_image_src($logo, 'large')[0]??'';
      }
      echo '<style>
         .login header,
@@ -501,7 +528,6 @@
   protected function renderForms():void
   {
      $this->metaForm = new MetaForm();
      $form = $this->action.'form';
      ?>
      <section class="login-box col btw">
@@ -520,7 +546,7 @@
            do_action('jvb_add_token_inputs', $this->action);
            foreach ($this->fields as $name => $config) {
               $this->metaForm->render($name, '', $config);
               echo Form::render($name, '', $config);
            }
            $this->maybeTurnstile();
@@ -597,7 +623,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>';
@@ -684,7 +711,7 @@
   protected function maybeTurnstile(): void
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return;
      }
      JVB()->connect('cloudflare')->renderTurnstile();
@@ -692,7 +719,7 @@
   protected function maybeTurnstileScripts(): void
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return;
      }
      JVB()->connect('cloudflare')->enqueueTurnstileScripts();
@@ -700,7 +727,7 @@
   protected function verifyTurnstile(): bool
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return true; // Not enabled, pass verification
      }
@@ -753,66 +780,25 @@
   {
      switch ($this->action) {
         case 'register':
            return [
               'title' => JVB_LOGIN['register']['title'] ?? 'Create Your Account',
               'description' => JVB_LOGIN['register']['description'] ?? [],
               'extra' => JVB_LOGIN['register']['extra'] ?? [],
               'footer' => JVB_LOGIN['register']['footer'] ?? '',
               'email' => JVB_LOGIN['register']['email']['subject'] ?? '['.get_bloginfo('name').'] Finish Creating Your Account',
               'submit' => JVB_LOGIN['register']['submit'] ?? 'Create Account',
               'successTitle' => JVB_LOGIN['register']['success']['title'] ?? 'Success!',
               'successDescription' => JVB_LOGIN['register']['success']['description'] ?? ['See your email for next steps','(Check your spam folder if you cannot find it after a couple minutes.)'],
            ];
            return Site::login()->getLabels('register');
         case 'lostpassword':
            return [
               'title' => JVB_LOGIN['forgot_password']['title'] ?? 'Reset Password',
               'description' => JVB_LOGIN['forgot_password']['description'] ?? [],
               'extra' => JVB_LOGIN['forgot_password']['extra'] ?? [],
               'footer' => JVB_LOGIN['forgot_password']['footer'] ?? '',
               'submit' => JVB_LOGIN['forgot_password']['submit'] ?? 'Send Reset Link',
               'successTitle' => JVB_LOGIN['forgot_password']['success']['title'] ?? 'Success!',
               'successDescription' => JVB_LOGIN['forgot_password']['success']['description'] ?? ['Check your email for reset instructions'],
            ];
            return Site::login()->getLabels('lostPassword');
         case 'resetpass':
            return [
               'title' => JVB_LOGIN['reset_pass']['title'] ?? 'Reset Your Password',
               'description' => JVB_LOGIN['reset_pass']['description'] ?? [],
               'extra' => JVB_LOGIN['reset_pass']['extra'] ?? [],
               'footer' => JVB_LOGIN['reset_pass']['footer'] ?? '',
               'submit' => JVB_LOGIN['reset_pass']['submit'] ?? 'Reset Password',
            ];
         case 'rp':
            return Site::login()->getLabels('resetPassword');
         case 'logout':
            return [
               'title' => JVB_LOGIN['logout']['title'] ?? 'Logged Out!',
               'description' => JVB_LOGIN['logout']['description'] ?? [],
               'extra' => JVB_LOGIN['logout']['extra'] ?? [],
               'footer' => JVB_LOGIN['logout']['footer'] ?? '',
               'submit' => JVB_LOGIN['logout']['submit'] ?? '',
            ];
            return Site::login()->getLabels('logout');
         case 'magic':
            return [
               'title' => JVB_LOGIN['magic']['title'] ?? 'Log in with Magic Link',
               'description' => JVB_LOGIN['magic']['description'] ?? ['Enter your email.','You\'ll get an email with a magic link.','Click it, and you\'re logged in!'],
               'extra' => JVB_LOGIN['magic']['extra'] ?? [],
               'footer' => JVB_LOGIN['magic']['footer'] ?? '',
               'submit' => JVB_LOGIN['magic']['submit'] ?? jvbIcon('magic-wand').'Send Magic Link',
            ];
            return Site::login()->getLabels('magic');
         case 'login':
         default:
            return [
               'title' => JVB_LOGIN['login']['title'] ?? 'Sign in',
               'description' => JVB_LOGIN['login']['description'] ?? [],
               'extra' => JVB_LOGIN['login']['extra'] ?? [],
               'footer' => JVB_LOGIN['login']['footer'] ?? '',
               'submit' => JVB_LOGIN['login']['submit'] ?? 'Sign In',
            ];
            return Site::login()->getLabels('login');
      }
   }
   protected function maybeMagicLink(): void
   {
      if (!$this->magicLink || !in_array($this->action, ['login', 'lostpassword'])) {
      if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) {
         return;
      }
      ?>
@@ -828,110 +814,119 @@
      SCRIPTS
   ************************************************************************/
   public function enqueueScripts(): void
   {
      if (!$this->isLoginPage()) {
         return;
      }
{
    if (!$this->isLoginPage()) {
        return;
    }
      $this->maybeTurnstileScripts();
      wp_enqueue_script('jvb-form');
      $action = $this->getAction();
      ob_start();
      ?>
    $this->maybeTurnstileScripts();
    wp_enqueue_script('jvb-form');
    $action = $this->getAction();
      document.addEventListener('DOMContentLoaded', () => {
         const form = document.querySelector('.login form');
         if (!form) return;
    $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : '';
    $has_turnstile = Site::hasIntegration('cloudflare');
         if (!window.jvbForm) {
            console.error('jvbForm not loaded');
            return;
         }
    ob_start();
         window.LoginController = new window.jvbForm();
         window.LoginController.registerForm(form, {
            autosave: false,
            endpoint: <?= "'{$action}'" ?>,
            formStatus: false,
            cache: false,
         });
    ?>
         window.LoginController.subscribe((event, data) => {
            if (event === 'form-submit') {
               handleFormSubmission(data);
            }
         });
   document.addEventListener('DOMContentLoaded', async function () {
      const hasTurnstile = <?= json_encode($has_turnstile) ?>;
      const redirectTo = <?= json_encode($redirect_to) ?>;
      window.auth.subscribe(event => {
         if (event === 'auth-loaded') {
            const form = document.querySelector('.login form');
            if (!form || !window.jvbForm) return;
         async function handleFormSubmission(data) {
         let realFormData = data.fullData;
         const { formId, config, data: formData } = data;
         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': jvbSettings.nonce
               },
               body: JSON.stringify(realFormData)
            window.jvbForm.registerForm(form, {
               endpoint: '<?= $action ?>',
               showStatus: false,
               cache: false,
            });
               const result = await response.json();
            window.jvbForm.subscribe((event, data) => {
               if (event === 'form-submit') {
                  const { config } = data;
                  const formElement = config.element;
               // Handle errors
               if (!response.ok) {
                  window.LoginController.showFormStatus(formId, 'error');
                  window.LoginController.handleFormError(form, result);
                  return;
                  // Collect current form data
                  const formData = new FormData(formElement);
                  const formObject = Object.fromEntries(formData.entries());
                  let params = new URLSearchParams(window.location.search);
                  if (params.has('key')) {
                     formObject['key'] = params.get('key');
                  }
                  if (params.has('login')) {
                     formObject['login'] = params.get('login');
                  }
                  // Add redirect_to from URL
                  if (redirectTo) {
                     formObject.redirect_to = redirectTo;
                  }
                  const submit = formElement.querySelector('[type=submit]');
                  const oldText = submit.textContent;
                  window.jvbForm.showFormStatus(config.id, 'uploading');
                  submit.disabled = true;
                  submit.textContent = 'Loading...';
                  window.auth.fetch(`${jvbSettings.api}auth/<?= $action ?>`, {
                     method: 'POST',
                     body: JSON.stringify(formObject)
                  })
                  .then(response => response.json().then(result => ({ response, result })))
                  .then(({ response, result }) => {
                     if (!response.ok) {
                        window.jvbForm.showFormStatus(config.id, 'error');
                        window.jvbForm.handleFormError(formElement, result);
                        return;
                     }
                     window.jvbForm.showFormStatus(config.id, 'submitted');
                     if (result.message) {
                        window.jvbForm.handleFormSuccess(formElement, result);
                     }
                     if (window.auth?.handleLogin && result.auth) {
                        return window.auth.handleLogin(result.auth).then(() => {
                           if (result.redirect) {
                              setTimeout(() => {
                                 window.location.href = result.redirect;
                              }, 100);
                           }
                        });
                     } else if (result.redirect) {
                        setTimeout(() => {
                           window.location.href = result.redirect;
                        }, 100);
                     }
                  })
                  .catch(error => {
                     console.error('Form submission error:', error);
                     window.jvbForm.showFormStatus(config.id, 'error');
                     window.jvbForm.handleFormError(formElement, {
                        message: 'Network error. Please check your connection and try again.',
                        code: 'network_error'
                     });
                  })
                  .finally(() => {
                     submit.textContent = oldText;
                     submit.disabled = false;
                  });
               }
               // Handle success
               window.LoginController.showFormStatus(formId, 'submitted');
               // Show success message briefly before redirect
               if (result.message) {
                  window.LoginController.handleFormSuccess(form, result);
               }
               // Handle redirect
               if (result.redirect) {
                  setTimeout(() => {
                     window.location.href = result.redirect;
                  }, 500); // Brief delay to show success message
               }
            } catch (error) {
               console.error('Form submission error:', error);
               window.LoginController.showFormStatus(formId, 'error');
               window.LoginController.handleFormError(form, {
                  message: 'Network error. Please check your connection and try again.',
                  code: 'network_error'
               });
            } finally {
               submit.textContent = oldText;
               submit.disabled = false;
            }
            });
         }
      });
   });
      <?php
   <?php
      $script = ob_get_clean();
      wp_add_inline_script('jvb-form', $script);
   }
@@ -962,6 +957,11 @@
      wp_safe_redirect($login_url);
      exit;
   }
   public function saveRegistrationFields(int $user_id, array $userdata):void
   {
   }
}
// Initialize the login manager