Jake Vanderwerf
7 days ago 46d681c6b825d21b3f698d793c4e630c687d90ad
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,11 +14,8 @@
class LoginManager
{
   protected Features $siteFeatures;
   protected ?MagicLinkManager $magicLink = null;
   protected ?MetaForm $metaForm = null;
   protected EmailManager $emailManager;
   protected AjaxRateLimiter $rateLimiter;
   protected ?Form $form = null;
   protected Cache $cache;
   protected array $forms =[];
@@ -28,9 +23,9 @@
   protected array $fields = [];
   protected ?string $action = null;
   protected string $title = '';
   protected static LoginManager $instance;
   // Token handlers registry
   protected array $tokenHandlers = [];
   protected array $messageHandlers = [];
   private array $allowed_file_types = [
@@ -41,18 +36,14 @@
   ];
   private int $max_file_size = 5242880; // 5MB in bytes
   public function __construct()
   {
      $this->siteFeatures = Features::forSite();
      $this->metaForm = new MetaForm();
      $this->emailManager = new EmailManager();
      $this->rateLimiter = new AjaxRateLimiter();
      // Register default token handlers
      $this->registerDefaultHandlers();
      self::$instance = $this;
      $this->cache = Cache::for('login');
      $this->cache->flush();
      // Initialize magic link support if enabled
      if ($this->siteFeatures->has('magicLink')) {
      if (Site::has('magicLink')) {
         $this->initMagicLinkSupport();
      }
@@ -66,19 +57,27 @@
        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15);
      // Handle form submissions via AJAX
      add_action('wp_ajax_nopriv_jvb_login', [$this, 'handleAjaxLogin']);
      add_action('wp_ajax_nopriv_jvb_register', [$this, 'handleAjaxRegister']);
      add_action('wp_ajax_nopriv_jvb_lostpassword', [$this, 'handleAjaxLostPassword']);
      add_action('wp_ajax_nopriv_jvb_resetpass', [$this, 'handleAjaxResetPassword']);
      // 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 static function getInstance():self
   {
      return self::$instance;
   }
   public function excludeLoginSitemap(array $ids): array
   {
      $ids[] = $this->getLoginPage();
      return $ids;
   }
   /**************************************************************************
      * SETUP & CONFIGURATION
   **************************************************************************/
@@ -88,12 +87,17 @@
    */
   public function redirectToCustomLogin(): void
   {
      // Handle interim login
      if (isset($_GET['interim-login'])) {
         // Don't redirect - let WP handle it
         return;
      }
      // Don't redirect if AJAX or REST
      if ((defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) {
         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
@@ -115,32 +119,40 @@
         $select = [];
         //Basic fields, for any
         $fields = [
            'name'   => [
            'user_name' => [
               'type'      => 'text',
               'required'  => true,
               'label'     => 'Your Name',
               'placeholder'=> 'Mister Meseeks'
            ],
            'email'  => [
            'user_email'   => [
               'type'      => 'email',
               'required'  => true,
               'label'     => 'Your Email',
               'placeholder'=> 'look@me.com'
            ]
         ];
         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'] ?? '';
         if (Site::has('referrals')) {
            $fields['referral_code'] = [
               'type'   => 'text',
               'required'=> false,
               'label'  => 'Referral Code',
               'hint'   => 'Have a referral code? Paste it here!'
            ];
         }
         $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;
@@ -178,12 +190,18 @@
   protected function setupFields():void
   {
      $this->fields = $this->getFieldsForAction($this->action);
   }
   protected function getFieldsForAction(string $action):array
   {
      $fields = [];
      switch($this->action) {
      switch($action) {
         case 'register':
            $fields = $this->getRegistrationFormFields();
            break;
         case 'lostpassword':
         case 'magic':
            $fields = [
               'user_email' => [
                  'type' => 'email',
@@ -216,12 +234,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' => [
@@ -247,9 +267,10 @@
            break;
      }
      $this->fields = $fields;
      return $fields;
   }
   /**
    * Ensure login page exists
    */
@@ -281,6 +302,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');
@@ -297,118 +352,13 @@
      return $self->isLoginPage();
   }
   /**************************************************************************
      TOKEN & MESSAGE HANDLERS
         Extensible by other classes
   **************************************************************************/
   public function registerTokenHandler(string $token_key, callable $handler, int $priority = 10): void
   {
      if (!isset($this->tokenHandlers[$priority])) {
         $this->tokenHandlers[$priority] = [];
      }
      $this->tokenHandlers[$priority][$token_key] = $handler;
      ksort($this->tokenHandlers);
   }
   public function registerMessageHandler(string $type, callable $handler, ?callable $condition = null): void
   {
      $this->messageHandlers[$type] = [
         'handler' => $handler,
         'condition' => $condition
      ];
   }
   protected function registerDefaultHandlers(): void
   {
      // Invitation handler
      if ($this->siteFeatures->has('invitations')) {
         $this->registerTokenHandler('invite', function($token, $email, $user_id) {
            if (isset($_POST['invite_token'])) {
               JVB()->routes('invites')->acceptInvitation(
                  sanitize_text_field($_POST['invite_token']),
                  sanitize_email($_POST['invite_email']),
                  $user_id
               );
            }
         });
         $this->registerMessageHandler('invitation',
            function() {
               $data = JVB()->routes('invites')->verifyInvitation(
                  sanitize_text_field($_GET['invite']),
                  sanitize_email($_GET['email'])
               );
               $name = $data->name;
               $inviters = json_decode($data->inviters, true);
               $names = [];
               foreach ($inviters as $inviter) {
                  $artist = jvbContentFromUser((int)$inviter['user_id']);
                  $names[] = ($artist['name'] === '') ? $artist['display_name'] : $artist['name'];
               }
               $message = (count($names) > 1)
                  ? 'are already here, and have invited you to join in!'
                  : ' is already here, and invited you to join in!';
               return '<h2>Join the Scene, '.$name.'</h2>
                    <p style="text-align:center;">'.jvbCommaList($names).$message.'</p>';
            },
            function() {
               return isset($_GET['invite']) && isset($_GET['email']);
            }
         );
      }
      // List sharing handler (Favourites)
      if ($this->siteFeatures->has('favourites')) {
         $this->registerTokenHandler('list_token', function($token, $email, $user_id) {
            if (!empty($_GET['list_token']) && !empty($_GET['email'])) {
               JVB()->routes('favourites')->acceptListInvitation(
                  sanitize_text_field($_GET['list_token']),
                  sanitize_email($_GET['email']),
                  $user_id
               );
            }
         });
         $this->registerMessageHandler('favourites',
            function() {
               return '<h2>'.(JVB_LOGIN['login_from_favourite_header'] ?? 'Save your Favourites').'</h2>';
            },
            function() {
               return isset($_GET['type']) && $_GET['type'] === 'favourites';
            }
         );
      }
      // Referral handler - FIXED VERSION
      $this->registerTokenHandler('referral_code', function($code, $email, $user_id) {
         // $code is already sanitized from processTokenHandlers
         if (session_status() === PHP_SESSION_NONE) {
            session_start();
         }
         $_SESSION[BASE . 'referral_code'] = $code;
         setcookie(
            BASE . 'referral_code',
            $code,
            time() + (86400 * 30),
            '/'
         );
      }, 5);
   }
   protected function initMagicLinkSupport(): void
   {
      if (!Features::forSite()->has('magicLink')) {
      if (!Site::has('magicLink')) {
         return;
      }
      $this->magicLink = new MagicLinkManager();
   }
   /*********************************************************************
      RENDERING
   *********************************************************************/
@@ -417,7 +367,24 @@
      if (!$this->isLoginPage()) {
         return $template;
      }
      global $_GET;
      if (is_user_logged_in() && (!array_key_exists('action', $_GET) || $_GET['action']!=='logout')) {
         wp_redirect(get_home_url(null, '/dash'));
         exit;
      }
      $this->setup();
      $page = $this->cache->remember(
            $this->getAction(),
            function() {
               return $this->renderPage();
            },
            5
         );
      echo $page;
      return '';
   }
   protected function renderPage() {
      ob_start();
      jvbInlineStyles('nav');
      jvbInlineStyles('dash');
@@ -428,11 +395,10 @@
      $this->renderForms();
      $this->renderFooter();
      echo  ob_get_clean();
      return '';
      return ob_get_clean();
   }
   protected function setup():void
   protected function getAction():string
   {
      if (array_key_exists('action', $_GET)) {
         switch ($_GET['action']){
@@ -450,8 +416,17 @@
      } else {
         $action = 'login';
      }
      return $action;
   }
      $this->action = $action;
   protected function setup():void
   {
      $this->action = $this->getAction();
      if ($this->action == 'logout' || array_key_exists('loggedout', $_GET)) {
         wp_logout();
         wp_redirect(esc_attr($_GET['redirect_to'] ?? get_home_url()));
         exit;
      }
      $this->setupLabels();
      $this->setupFields();
      $this->setupTitle();
@@ -478,11 +453,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,
@@ -496,6 +469,12 @@
            justify-content: center;
            position: relative;
         }
         .login .fstatus.fstatus {
            --wrap: nowrap;
            top:0;
            bottom:unset;
            right: 0;
         }
         .login main::before {
            background-size: 20vw;
            inset: 0;
@@ -509,8 +488,8 @@
         .login main .login-box {
            --gap: .75rem;
            padding: 1rem;
            background-color:rgba(var(--base-rgb),var(--op-6));
            border-radius: var(--outerRadius);
            background-color: var(--overlay-heavy);
            box-shadow: var(--shadow-right), var(--shadow-down);
            margin: 15vh auto 0!important;
         }
@@ -540,7 +519,8 @@
            .login main .navigation,
            .login main .login-box {
               max-width: 60vw!important;
               margin: 0 2rem 0 auto!important;
               padding-right: 4rem!important;
               margin: 0 0 0 auto!important;
            }
            .login main .login-box {
               padding: 2rem;
@@ -567,33 +547,14 @@
   protected function renderForms():void
   {
      $form = $this->action.'form';
      ?>
      <section class="login-box col btw">
      <section class="login-box col y-btw">
         <h1><?=$this->labels['title']?></h1>
         <?= $this->labels['description'] ?>
         <form name="<?=$form?>" method="post" data-action="jvb_<?=$this->action?>">
            <?php wp_nonce_field('jvb_'.$this->action, '_wpnonce'); ?>
            <input type="hidden" name="action" value="jvb_<?=$this->action?>">
            <input type="hidden" name="redirect_to" value="<?= esc_attr($_GET['redirect_to'] ?? '') ?>">
            <input type="hidden" name="request_id" value="<?= wp_generate_password(16, false) ?>">
            <?php
            $this->addHiddenTokenFields();
         <?= $this->renderLoginForm($this->action); ?>
            foreach ($this->fields as $name => $config) {
               $this->metaForm->render($name, '', $config);
            }
            $this->maybeTurnstile();
             ?>
             <div class="row btw nowrap">
               <button type="submit" class="button button-primary button-large">Log In</button>
               <?php $this->maybeMagicLink(); ?>
            </div>
         </form>
         <?php
         if (is_array($this->labels['extra'])) {
@@ -607,7 +568,7 @@
         }
         ?>
         <div class="options row btw">
         <div class="options row x-btw">
            <?php
            switch ($this->action) {
               case 'login': ?>
@@ -620,7 +581,8 @@
                  <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
                  <?php
                  break;
               case 'lostpassword': ?>
               case 'lostpassword':
               case 'magic': ?>
                  <a href="<?= get_the_permalink() ?>">Login Instead</a>
                  <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
                  <?php
@@ -631,7 +593,7 @@
         </div>
      </section>
      <div class="navigation row btw">
      <div class="navigation row x-btw">
         <a href="<?= get_home_url() ?>">Home</a>
         <?php
         $privacy = get_privacy_policy_url();
@@ -641,6 +603,59 @@
      </div>
      <?php
   }
   public function renderLoginForm(string $action = 'login', string $redirect = '', string $title = ''):string
   {
      ob_start();
      do_action('jvb_add_token_inputs', $this->action);
      $additionalInputs = ob_get_clean();
      $fields = '';
      $theFields = $this->getFieldsForAction($action);
      foreach ($theFields as $name => $config) {
         $fields .= Form::render($name, '', $config);
      }
      ob_start();
      $this->maybeTurnstile();
      $turnstile = ob_get_clean();
      ob_start();
      $this->maybeMagicLink();
      $magicLink = ob_get_clean();
      $redirect = !empty($redirect) ? $redirect : esc_attr($_GET['redirect_to'] ?? '');
      return sprintf(
         '<form name="%sform" method="post" data-action="jvb_%s">
            %s%s%s
            <input type="hidden" name="action" value="jvb_%s">
            <input type="hidden" name="redirect_to" value="%s">
            <input type="hidden" name="request_id" value="%s">
            %s
            %s
            %s
            %s
             <div class="row x-btw nowrap">
               <button type="submit" class="button button-primary button-large">%s</button>
               %s
            </div>
         </form>',
         $action,
         $action,
         jvbFormStatus(),
         $title,
         wp_nonce_field('jvb_'.$action),
         $action,
         $redirect,
         wp_generate_password(16, false),
         ($action === 'magic') ? '<input type="hidden" name="type" value="login">' : '',
         $additionalInputs,
         $fields,
         $turnstile,
         $this->labels['submit'],
         $magicLink
      );
   }
   protected function renderHeader():void
    {
    ?>
@@ -659,10 +674,11 @@
            <?php
            $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">'.
               jvbIcon('light', ['title'=> 'Light Mode']).
               jvbIcon('dark', ['title'=>'Dark Mode']).
            echo '<label title="'.$title.'" id="theme-switch" class="switch" for="theme-switcher">
               <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>';
            ?>
            <p class="title">
@@ -677,7 +693,7 @@
                  }
               }
               if ($out == '') {
                  $out =jvbIcon('home');
                  $out =jvbIcon('house');
               }
                    ?><?= $out ?>
                </a>
@@ -709,339 +725,6 @@
        <?php
    }
   protected function addHiddenTokenFields(): void
   {
      foreach ($this->tokenHandlers as $priority => $handlers) {
         foreach ($handlers as $token_key => $handler) {
            if (isset($_GET[$token_key])) {
               $value = sanitize_text_field($_GET[$token_key]);
               echo '<input type="hidden" name="' . esc_attr($token_key) . '" value="' . esc_attr($value) . '">';
            }
         }
      }
      if (isset($_GET['email'])) {
         echo '<input type="hidden" name="token_email" value="' . esc_attr(sanitize_email($_GET['email'])) . '">';
      }
   }
   /*************************************************************************
      AJAX HANDLERS
   *************************************************************************/
   public function handleAjaxLogin(): void
   {
      check_ajax_referer('jvb_login', '_wpnonce');
      // Rate limiting
      if (!$this->checkAjaxRateLimit('login')) {
         wp_send_json_error([
            'message' => 'Too many attempts. Please wait a moment.',
            'code' => 'rate_limit'
         ], 429);
      }
      // Duplicate submission check
      if (!$this->checkRequestId()) {
         wp_send_json_error([
            'message' => 'Duplicate request detected',
            'code' => 'duplicate_request'
         ], 409);
      }
      $email = sanitize_email($_POST['user_email'] ?? '');
      $password = $_POST['user_password'] ?? '';
      $remember = !empty($_POST['remember_me']);
      if (empty($email) || empty($password)) {
         wp_send_json_error([
            'message' => 'Please fill in all fields',
            'field' => empty($email) ? 'user_email' : 'user_password',
            'code' => 'missing_fields'
         ]);
      }
      // Verify Turnstile if enabled
      if (!$this->verifyTurnstile()) {
         wp_send_json_error([
            'message' => 'Security verification failed',
            'code' => 'turnstile_failed'
         ]);
      }
      $user = get_user_by('email', $email);
      if (!$user) {
         wp_send_json_error([
            'message' => 'Unknown email address',
            'field' => 'user_email',
            'code' => 'invalid_email'
         ]);
      }
      $user = wp_authenticate($user->user_login, $password);
      if (is_wp_error($user)) {
         wp_send_json_error([
            'message' => $user->get_error_message(),
            'field' => 'user_password',
            'code' => $user->get_error_code()
         ]);
      }
      wp_clear_auth_cookie();
      wp_set_current_user($user->ID);
      wp_set_auth_cookie($user->ID, $remember);
      do_action('wp_login', $user->user_login, $user);
      $redirect = $_POST['redirect_to'] ?? home_url('/dash');
      wp_send_json_success(['redirect' => $redirect]);
   }
   public function handleAjaxRegister(): void
   {
      check_ajax_referer('jvb_register', '_wpnonce');
      // Rate limiting
      if (!$this->checkAjaxRateLimit('register')) {
         wp_send_json_error([
            'message' => 'Too many attempts. Please wait a moment.',
            'code' => 'rate_limit'
         ], 429);
      }
      // Duplicate submission check
      if (!$this->checkRequestId()) {
         wp_send_json_error([
            'message' => 'Duplicate request detected',
            'code' => 'duplicate_request'
         ], 409);
      }
      // Verify Turnstile
      if (!$this->verifyTurnstile()) {
         wp_send_json_error([
            'message' => 'Security verification failed',
            'code' => 'turnstile_failed'
         ]);
      }
      $name = sanitize_text_field($_POST['name'] ?? '');
      $email = sanitize_email($_POST['email'] ?? '');
      $user_type = sanitize_text_field($_POST['user_select'] ?? 'subscriber');
      // Spam prevention - if subscriber is selected and there are other options
      if ($user_type === 'subscriber' && count(JVB_USER) > 0) {
         $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false);
         if (!empty($registerable)) {
            wp_send_json_error([
               'message' => 'Please select a valid account type',
               'field' => 'user_select',
               'code' => 'invalid_user_type'
            ]);
         }
      }
      // Validate fields
      if (empty($name)) {
         wp_send_json_error([
            'message' => 'Name is required',
            'field' => 'name',
            'code' => 'missing_name'
         ]);
      }
      if (empty($email)) {
         wp_send_json_error([
            'message' => 'Email is required',
            'field' => 'email',
            'code' => 'missing_email'
         ]);
      }
      // Check if role can register
      if ($user_type !== 'subscriber') {
         if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) {
            wp_send_json_error([
               'message' => 'Invalid account type',
               'field' => 'user_select',
               'code' => 'invalid_user_type'
            ]);
         }
      }
      // Check if email exists
      if (email_exists($email)) {
         wp_send_json_error([
            'message' => 'Email already registered',
            'field' => 'email',
            'code' => 'duplicate_email'
         ]);
      }
      // Create user
      $user_id = wp_create_user($email, wp_generate_password(), $email);
      if (is_wp_error($user_id)) {
         wp_send_json_error([
            'message' => $user_id->get_error_message(),
            'code' => 'user_creation_failed'
         ]);
      }
      // Update user data
      wp_update_user([
         'ID' => $user_id,
         'display_name' => $name,
         'first_name' => $name
      ]);
      // Set role
      $user = new WP_User($user_id);
      if ($user_type === 'subscriber') {
         $user->set_role('subscriber');
      } else {
         $role = JVB_USER[$user_type]['role'] ?? 'subscriber';
         $user->set_role($role);
         // Check if needs approval
         if (Features::forMembership()->has('memberVerified') &&
            in_array($role, JVB_MEMBERSHIP['memberVerified'] ?? [])) {
            $user->add_cap('skip_moderation', false);
            update_user_meta($user_id, BASE . 'pending_approval', true);
         }
      }
      // Save additional fields
      update_user_meta($user_id, BASE . 'user_type', $user_type);
      // Process additional fields from form
      foreach ($_POST as $key => $value) {
         if (in_array($key, ['name', 'email', 'action', '_wpnonce', 'request_id', 'user_select'])) {
            continue;
         }
         update_user_meta($user_id, BASE . $key, sanitize_text_field($value));
      }
      // Handle token handlers
      $this->processTokenHandlers($user_id, $email);
      // Send welcome email with password setup link
      $this->sendWelcomeEmail($user_id);
      // Trigger registration action for other systems
      do_action('jvbAfterUserRegistration', $user_id, $user_type, $_POST);
      wp_send_json_success([
         'message' => 'Registration successful! Check your email.',
         'title' => $this->labels['successTitle'] ?? 'Success!',
         'description' => $this->labels['successDescription'] ?? 'Check your email for next steps',
         'user_id' => $user_id // Important for file upload dependencies!
      ]);
   }
   public function handleAjaxLostPassword(): void
   {
      check_ajax_referer('jvb_lostpassword', '_wpnonce');
      // Rate limiting
      if (!$this->checkAjaxRateLimit('lostpassword')) {
         wp_send_json_error([
            'message' => 'Too many attempts. Please wait a moment.',
            'code' => 'rate_limit'
         ], 429);
      }
      $email = sanitize_email($_POST['user_email'] ?? '');
      if (empty($email)) {
         wp_send_json_error([
            'message' => 'Email required',
            'field' => 'user_email',
            'code' => 'missing_email'
         ]);
      }
      // Verify Turnstile
      if (!$this->verifyTurnstile()) {
         wp_send_json_error([
            'message' => 'Security verification failed',
            'code' => 'turnstile_failed'
         ]);
      }
      // Use WordPress's built-in function
      $result = retrieve_password($email);
      if (is_wp_error($result)) {
         wp_send_json_error([
            'message' => $result->get_error_message(),
            'code' => $result->get_error_code()
         ]);
      }
      wp_send_json_success(['message' => 'Check your email for reset link']);
   }
   public function handleAjaxResetPassword(): void
   {
      check_ajax_referer('jvb_resetpass', '_wpnonce');
      // Rate limiting
      if (!$this->checkAjaxRateLimit('resetpass')) {
         wp_send_json_error([
            'message' => 'Too many attempts. Please wait a moment.',
            'code' => 'rate_limit'
         ], 429);
      }
      $key = sanitize_text_field($_POST['key'] ?? $_GET['key'] ?? '');
      $login = sanitize_text_field($_POST['login'] ?? $_GET['login'] ?? '');
      $pass1 = $_POST['pass1'] ?? '';
      $pass2 = $_POST['pass2'] ?? '';
      if (empty($key) || empty($login)) {
         wp_send_json_error([
            'message' => 'Invalid reset link',
            'code' => 'invalid_key'
         ]);
      }
      if (empty($pass1) || empty($pass2)) {
         wp_send_json_error([
            'message' => 'Please enter a password',
            'field' => empty($pass1) ? 'pass1' : 'pass2',
            'code' => 'missing_password'
         ]);
      }
      if ($pass1 !== $pass2) {
         wp_send_json_error([
            'message' => 'Passwords do not match',
            'field' => 'pass2',
            'code' => 'password_mismatch'
         ]);
      }
      // Verify reset key
      $user = check_password_reset_key($key, $login);
      if (is_wp_error($user)) {
         wp_send_json_error([
            'message' => 'Invalid or expired reset link',
            'code' => 'invalid_key'
         ]);
      }
      // Reset password
      reset_password($user, $pass1);
      wp_send_json_success([
         'message' => 'Password reset successfully',
         'redirect' => home_url('/login')
      ]);
   }
   /**********************************************************************
      TOKEN PROCESSING
   **********************************************************************/
@@ -1057,47 +740,10 @@
      }
   }
   /***********************************************************************
      EMAIL SENDING
   ***********************************************************************/
   protected function sendWelcomeEmail(int $user_id): void
   {
      $user = get_userdata($user_id);
      if (!$user) {
         return;
      }
      // Generate password reset key
      $key = get_password_reset_key($user);
      if (is_wp_error($key)) {
         error_log('Failed to generate password reset key: ' . $key->get_error_message());
         return;
      }
      $reset_url = add_query_arg([
         'action' => 'rp',
         'key' => $key,
         'login' => rawurlencode($user->user_login)
      ], home_url('/login'));
      $subject = $this->labels['email'] ?? 'Welcome to ' . get_bloginfo('name');
      $message = '<h2>Welcome, ' . esc_html($user->display_name) . '!</h2>';
      $message .= '<p>Your account has been created. Click the button below to set your password and get started:</p>';
      $message .= jvbMailButton($reset_url, 'Set Your Password');
      $message .= '<p>This link expires in 24 hours.</p>';
      $this->emailManager->sendEmail($user->user_email, $subject, $message);
   }
   /*************************************************************************
   *  SECURITY & VALIDATION
   *************************************************************************/
   protected function checkAjaxRateLimit(string $action): bool
   {
      return $this->rateLimiter->checkLimit($action);
   }
   protected function checkRequestId(): bool
   {
      $request_id = $_POST['request_id'] ?? '';
@@ -1117,7 +763,7 @@
   protected function maybeTurnstile(): void
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return;
      }
      JVB()->connect('cloudflare')->renderTurnstile();
@@ -1125,7 +771,7 @@
   protected function maybeTurnstileScripts(): void
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return;
      }
      JVB()->connect('cloudflare')->enqueueTurnstileScripts();
@@ -1133,7 +779,7 @@
   protected function verifyTurnstile(): bool
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return true; // Not enabled, pass verification
      }
@@ -1151,97 +797,67 @@
   protected function setupLabels(): void
   {
      $default = $this->getDefaultLabels();
      $this->labels = apply_filters('jvbLoginLabels', $default, $_GET);
      $default = apply_filters('jvbLoginLabels', $default, $_GET);
      foreach (['description', 'footer', 'extra'] as $location) {
         $text = (!is_array($this->labels[$location])) ? [$this->labels[$location]] : $this->labels[$location];
         if (!empty($text)) {
            $this->labels[$location] = '<div class="'.$location.'">';
            foreach ($text as $d) {
               $this->labels[$location] .= '<p>'.$d.'</p>';
      if(array_key_exists('type', $_GET) && $_GET['type'] === 'favourites') {
         if (array_key_exists('favourites', JVB_LOGIN)) {
            foreach (JVB_LOGIN['favourites'] as $key => $value) {
               $default[$key] = $value;
            }
            $this->labels[$location] .= '</div>';
         }
      }
      foreach (['description', 'footer', 'extra'] as $location) {
         if ($default[$location] === '') {
            continue;
         }
         if (empty($default[$location])) {
            $default[$location] = '';
            continue;
         }
         $text = (!is_array($default[$location])) ? [$default[$location]] : $default[$location];
         if (!empty($text)) {
            $default[$location] = '<div class="'.$location.'">';
            foreach ($text as $d) {
               $default[$location] .= '<p>'.$d.'</p>';
            }
            $default[$location] .= '</div>';
         }
      }
      $this->labels = $default;
   }
   protected function getDefaultLabels(): array
   {
      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 Site::login()->getLabels('logout');
         case 'magic':
            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;
      }
      ?>
      <button type="button" id="magic-link-btn" class="button button-secondary button-large">
         <?= jvbIcon('email', ['size' => 20]); ?>
         Get Login Link
      </button>
      <script type="text/javascript">
         document.getElementById('magic-link-btn')?.addEventListener('click', function(e) {
            e.preventDefault();
            const email = document.querySelector('input[name="user_email"]')?.value;
            if (!email) {
               alert('Please enter your email address first');
               return;
            }
            fetch('<?= rest_url('jvb/v1/magic-link'); ?>', {
               method: 'POST',
               headers: {
                  'Content-Type': 'application/json',
                  'X-WP-Nonce': '<?= wp_create_nonce('wp_rest') ?>'
               },
               body: JSON.stringify({ email: email, type: 'login' })
            })
            .then(r => r.json())
            .then(data => {
               alert(data.success ? 'Check your email!' : (data.message || 'Failed to send link'));
            });
         });
      </script>
      <a class="button" href="<?= add_query_arg('action', 'magic', wp_login_url()) ?>" title="Email yourself a link to log you in auto-magically!">
         <?= jvbIcon('magic-wand'); ?>
         Magic Link
      </a>
      <?php
   }
@@ -1250,28 +866,119 @@
      SCRIPTS
   ************************************************************************/
   public function enqueueScripts(): void
   {
      if (!$this->isLoginPage()) {
         return;
      }
{
    if (!$this->isLoginPage()) {
        return;
    }
      $this->maybeTurnstileScripts();
      wp_enqueue_script('jvb-form');
    $this->maybeTurnstileScripts();
    wp_enqueue_script('jvb-form');
    $action = $this->getAction();
      $script = "
      document.addEventListener('DOMContentLoaded', () => {
         const form = document.querySelector('.login form');
         if (form && window.jvbForm) {
            let controller = new window.jvbForm();
            controller.registerForm(form, {
               autosave: false,
               endpoint: false
    $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : '';
    $has_turnstile = Site::hasIntegration('cloudflare');
    ob_start();
    ?>
   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;
            window.jvbForm.registerForm(form, {
               endpoint: '<?= $action ?>',
               showStatus: false,
               cache: false,
            });
         } else if (form && !window.jvbForm) {
            console.error('jvbForm not loaded');
         }
      });";
            window.jvbForm.subscribe((event, data) => {
               if (event === 'form-submit') {
                  const { config } = data;
                  const formElement = config.element;
                  // 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;
                              }, 20);
                           }
                        });
                     } else if (result.redirect) {
                        setTimeout(() => {
                           window.location.href = result.redirect;
                        }, 20);
                     }
                  })
                  .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;
                  });
               }
            });
         }
      });
   });
   <?php
      $script = ob_get_clean();
      wp_add_inline_script('jvb-form', $script);
   }
@@ -1302,6 +1009,16 @@
      wp_safe_redirect($login_url);
      exit;
   }
   public function saveRegistrationFields(int $user_id, array $userdata):void
   {
   }
   public function setAction(string $action = 'login'):void
   {
      $this->action = $action;
      $this->setup();
   }
}
// Initialize the login manager