Jake Vanderwerf
2026-05-01 48721c85ebcfa973ee81719d2467ca80e4253dc9
inc/rest/routes/LoginRoutes.php
@@ -1,11 +1,10 @@
<?php
namespace JVBase\rest\routes;
use JVBase\managers\EmailManager;
use JVBase\managers\LoginManager;
use JVBase\managers\MagicLinkManager;
use JVBase\rest\RestRouteManager;
use JVBase\utility\Features;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use JVBase\base\Site;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
@@ -15,191 +14,467 @@
   exit;
}
class LoginRoutes extends RestRouteManager
/**
 * Login Routes
 *
 * Handles all authentication-related endpoints with session security
 */
class LoginRoutes extends Rest
{
   protected EmailManager $emailManager;
   protected MagicLinkManager $magic_link;
   protected LoginManager $loginManager;
   protected ?string $requestId = null;
   protected bool $hasMagicLink = false;
   public function __construct()
   {
      $this->cache_name = 'auth';
      $this->cache_ttl = WEEK_IN_SECONDS;
      $this->emailManager = new EmailManager();
      if (Features::forSite()->has('magicLink')) {
         $this->magic_link = new MagicLinkManager();
      }
      $this->cacheName = 'auth';
      $this->cacheTtl = WEEK_IN_SECONDS;
      parent::__construct();
      $this->hasMagicLink = Site::has('magicLink');
   }
   public function registerRoutes(): void
   {
      // Login endpoint
      register_rest_route($this->namespace, '/auth/login', [
         'methods' => 'POST',
         'callback' => [$this, 'handleLogin'],
         'permission_callback' => [$this, 'checkRateLimit'],
         'args' => [
            'user_email' => [
               'required' => true,
               'type' => 'string',
               'sanitize_callback' => 'sanitize_email'
            ],
            'user_password' => [
               'required' => true,
               'type' => 'string'
            ],
            'remember_me' => [
               'required' => false,
               'type' => 'boolean',
               'default' => false
            ]
         ]
      ]);
      // Auth status endpoint
      Route::for('auth/status')
         ->get([$this, 'getAuthStatus'])
         ->auth('public')
         ->rateLimit()
         ->register();
      // Logout endpoint
      register_rest_route($this->namespace, '/auth/logout', [
         'methods' => 'POST',
         'callback' => [$this, 'handleLogout'],
         'permission_callback' => 'is_user_logged_in'
      ]);
      // Standard login
      Route::for('auth/login')
         ->post([$this, 'handleLogin'])
         ->args([
            'user_email' => 'email|required',
            'user_password' => 'string|required',
            'remember_me' => 'boolean',
            'redirect_to' => 'string',
         ])
         ->auth('public')
         ->rateLimit(5, 300)
         ->register();
      // Check auth status
      register_rest_route($this->namespace, '/auth/status', [
         'methods' => 'GET',
         'callback' => [$this, 'getAuthStatus'],
         'permission_callback' => '__return_true'
      ]);
      // User registration
      Route::for('auth/register')
         ->post([$this, 'handleRegister'])
         ->args([
            'user_name' => 'string|required',
            'user_email' => 'email|required',
            'user_select' => 'string',
            'referral_code' => 'string',
            'redirect_to' => 'string',
         ])
         ->auth('public')
         ->rateLimit(3, 3600)
         ->register();
      // Request password reset
      register_rest_route($this->namespace, '/auth/lostpassword', [
         'methods' => 'POST',
         'callback' => [$this, 'requestPasswordReset'],
         'permission_callback' => [$this, 'checkRateLimit'],
         'args' => [
            'user_email' => [
               'required' => true,
               'type' => 'string',
               'format' => 'email',
               'sanitize_callback' => 'sanitize_email'
            ]
         ]
      ]);
      Route::for('auth/lostpassword')
         ->post([$this, 'handleLostPassword'])
         ->args([
            'user_email' => 'email|required',
         ])
         ->auth('public')
         ->rateLimit(3, 3600)
         ->register();
      // Complete password reset (with token)
      register_rest_route($this->namespace, '/auth/reset-password', [
         'methods' => 'POST',
         'callback' => [$this, 'resetPassword'],
         'permission_callback' => [$this, 'checkRateLimit'],
         'args' => [
            'token' => [
               'required' => true,
               'type' => 'string',
               'sanitize_callback' => 'sanitize_text_field'
            ],
            'user_email' => [
               'required' => true,
               'type' => 'string',
               'format' => 'email',
               'sanitize_callback' => 'sanitize_email'
            ],
            'password' => [
               'required' => true,
               'type' => 'string'
            ]
         ]
      ]);
      // Reset password with token
      Route::for('auth/resetpass')
         ->post([$this, 'handleResetPassword'])
         ->args([
            'key' => 'string|required',
            'login' => 'string|required',
            'pass1' => 'string|required',
            'pass2' => 'string|required',
         ])
         ->auth('public')
         ->rateLimit(5, 300)
         ->register();
      register_rest_route($this->namespace, '/auth/register', [
         'method' => 'POST',
         'callback'  => [$this, 'handleRegister'],
         'permission_callback' => [$this, 'checkRateLimit'],
      ]);
      // Refresh session
      register_rest_route($this->namespace, '/auth/refresh', [
         'methods' => 'POST',
         'callback' => [$this, 'refreshSession'],
         'permission_callback' => 'is_user_logged_in'
      ]);
      register_rest_route($this->namespace, '/auth/email', [
         'methods'   => 'POST',
         'callback'  => [$this, 'checkEmailExists'],
         'permission_callback' => [$this, 'checkRateLimit'],
      ]);
   }
   public function handleLogin(WP_REST_Request $request): WP_REST_Response
   {
      // Verify Turnstile
      if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
         return $this->error('Security verification failed', 'turnstile_failed', 403);
      // Magic link endpoint
      if ($this->hasMagicLink) {
         Route::for('auth/magic')
            ->post([$this, 'handleMagicLink'])
            ->args([
               'user_email' => 'email|required',
               'type' => 'string|enum:login,signup,referral',
               'redirect_to' => 'string',
            ])
            ->auth('public')
            ->rateLimit(5, 3600)
            ->register();
      }
      $username = $request->get_param('user_email');
      $password = $request->get_param('user_password');
      $remember = (bool)$request->get_param('remember_me');
      // Logout endpoint
      Route::for('auth/logout')
         ->post([$this, 'handleLogout'])
         ->auth('logged_in')
         ->rateLimit(10)
         ->register();
   }
      // Check for account lockout
      $lockout = $this->checkAccountLockout($username);
      if (is_wp_error($lockout)) {
   /**
    * Get authentication status
    */
   public function getAuthStatus(WP_REST_Request $request): WP_REST_Response
   {
      $data = $this->buildAuth();
      $response = $this->success($data);
      // Add caching headers
      $response->header('Cache-Control', 'private, max-age=300'); // 5 minutes
      $response->header('Vary', 'Cookie'); // Important for nginx
      return $response;
   }
   /**
    * Handle standard login
    */
   public function handleLogin(WP_REST_Request $request): WP_REST_Response
   {
      $email = sanitize_email($request->get_param('user_email'));
      $password = $request->get_param('user_password');
      $remember = (bool) $request->get_param('remember_me');
      $redirect_to = $request->get_param('redirect_to');
      // Verify Turnstile
      if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
         return $this->error(
            $lockout->get_error_message(),
            'account_locked',
            429
            'Security verification failed. Please try again.',
            'turnstile_failed',
            403
         );
      }
      // Attempt login
      $user = wp_signon([
         'user_login'   => $username,
         'user_email'   => $username,
         'user_password' => $password,
         'remember' => $remember
      ], false);
      // Attempt authentication
      $user = wp_authenticate($email, $password);
      if (is_wp_error($user)) {
         // Track failed attempt
         $this->trackFailedLogin($username);
         $this->auditLog('login_failed', [
            'email' => $email,
            'error' => $user->get_error_code(),
         ]);
         return $this->error(
            'Invalid username or password',
            'login_failed',
            $this->getLoginErrorMessage($user),
            $user->get_error_code(),
            401
         );
      }
      // Clear failed attempts on success
      $this->clearFailedAttempts($username);
      // Set auth cookie with remember me flag
      // Set auth cookie
      wp_clear_auth_cookie();
      wp_set_current_user($user->ID);
      wp_set_auth_cookie($user->ID, $remember);
      // Store session fingerprint for hijacking protection
      // Store session fingerprint
      $this->storeSessionFingerprint($user->ID, $request);
      // Trigger WordPress login action
      do_action('wp_login', $user->user_login, $user);
      // Audit log
      $this->auditLog('user_login', [
      $this->auditLog('login_success', [
         'user_id' => $user->ID,
         'remember' => $remember
         'email' => $email,
      ]);
      // Get redirect URL
      $redirect = $this->getRedirectUrl($user, $redirect_to);
      // Return auth data for frontend
      return $this->success([
         'message' => 'Login successful',
         'user' => $this->formatUserData($user),
         'redirect' => $this->getLoginRedirect($user)
         'redirect' => $redirect,
         'auth' => $this->buildAuth($user->ID)
      ]);
   }
   /**
    * Handle logout request
    * Handle user registration
    */
   public function handleRegister(WP_REST_Request $request): WP_REST_Response
   {
      $email = sanitize_email($request->get_param('user_email'));
      $name = sanitize_text_field($request->get_param('user_name'));
      $user_select = sanitize_text_field($request->get_param('user_select') ?? 'subscriber');
      $referral_code = sanitize_text_field($request->get_param('referral_code') ?? '');
      // Verify Turnstile
      if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
         return $this->error(
            'Security verification failed. Please try again.',
            'turnstile_failed',
            403
         );
      }
      // Check if email already exists
      if (email_exists($email)) {
         return $this->error(
            'An account with this email already exists.',
            'email_exists',
            400,
            'user_email'
         );
      }
      // Validate role selection
      $role = $this->validateUserRole($user_select);
      if (is_wp_error($role)) {
         return $this->error(
            $role->get_error_message(),
            'invalid_role',
            400,
            'user_select'
         );
      }
      // Create user account
      $user_id = wp_create_user(
         $email,
         wp_generate_password(20, true, true),
         $email
      );
      if (is_wp_error($user_id)) {
         $this->logError('Registration failed', [
            'email' => $email,
            'error' => $user_id->get_error_message(),
         ]);
         return $this->error(
            'Failed to create account. Please try again.',
            'registration_failed',
            500
         );
      }
      // Set user details
      $user = get_user_by('ID', $user_id);
      $user->set_role($role);
      wp_update_user([
         'ID' => $user_id,
         'display_name' => $name,
         'first_name' => $name,
      ]);
      // Process referral code if provided
      if (!empty($referral_code) && Site::has('referrals')) {
         $this->processReferralCode($user_id, $referral_code);
      }
      // Process additional registration fields
      $this->processRegistrationFields($user_id, $request->get_params());
      do_action('user_register', $user_id, $request->get_params());
      // Send magic link for email verification
      if ($this->hasMagicLink) {
         JVB()->magicLink()->sendMagicLink($email, 'signup', [
            'name' => $name,
            'role' => $role,
         ]);
      }
      $this->auditLog('registration_success', [
         'user_id' => $user_id,
         'email' => $email,
         'role' => $role,
      ]);
      return $this->success([
         'message' => 'Registration successful! Check your email to complete setup.',
         'title' => 'Success!',
         'description' => [
            'See your email for next steps',
            '(Check your spam folder if you cannot find it after a couple minutes.)'
         ]
      ]);
   }
   /**
    * Handle lost password request
    */
   public function handleLostPassword(WP_REST_Request $request): WP_REST_Response
   {
      $email = sanitize_email($request->get_param('user_email'));
      // Verify Turnstile
      if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
         return $this->error(
            'Security verification failed. Please try again.',
            'turnstile_failed',
            403
         );
      }
      // Check if user exists
      $user = get_user_by('email', $email);
      if (!$user) {
         // Don't reveal if email exists for security
         return $this->success([
            'message' => 'If that email address is in our system, we\'ve sent a password reset link.',
            'title' => 'Success!',
            'description' => ['Check your email for reset instructions']
         ]);
      }
      // Use magic link if available, otherwise standard WP reset
      if ($this->hasMagicLink) {
         $result = JVB()->magicLink()->sendMagicLink($email, 'reset');
         if (is_wp_error($result)) {
            $this->logError('Magic link send failed', [
               'email' => $email,
               'error' => $result->get_error_message(),
            ]);
         }
      } else {
         // Standard WordPress password reset
         $key = get_password_reset_key($user);
         if (is_wp_error($key)) {
            $this->logError('Reset key generation failed', [
               'email' => $email,
               'error' => $key->get_error_message(),
            ]);
         } else {
            $success = JVB()->email()->sendPasswordResetEmail($user, $key);
         }
      }
      $this->auditLog('password_reset_requested', [
         'user_id' => $user->ID,
         'email' => $email,
      ]);
      return $this->success([
         'message' => 'Check your email for reset instructions.',
         'title' => 'Success!',
         'description' => ['Check your email for reset instructions']
      ]);
   }
   /**
    * Handle password reset with token
    */
   public function handleResetPassword(WP_REST_Request $request): WP_REST_Response
   {
      $key = sanitize_text_field($request->get_param('key'));
      $login = sanitize_text_field($request->get_param('login'));
      $pass1 = $request->get_param('pass1');
      $pass2 = $request->get_param('pass2');
      // Verify passwords match
      if ($pass1 !== $pass2) {
         return $this->error(
            'Passwords do not match.',
            'password_mismatch',
            400,
            'pass2'
         );
      }
      // Validate password strength
      if (strlen($pass1) < 8) {
         return $this->error(
            'Password must be at least 8 characters.',
            'password_weak',
            400,
            'pass1'
         );
      }
      // Verify reset key
      $user = check_password_reset_key($key, $login);
      if (is_wp_error($user)) {
         return $this->error(
            'Invalid or expired reset link.',
            'invalid_key',
            400
         );
      }
      // Reset password
      reset_password($user, $pass1);
      $this->auditLog('password_reset_completed', [
         'user_id' => $user->ID,
      ]);
      return $this->success([
         'message' => 'Password reset successful! You can now log in.',
         'redirect' => wp_login_url(),
      ]);
   }
   /**
    * Handle magic link request
    */
   public function handleMagicLink(WP_REST_Request $request): WP_REST_Response
   {
      if (!$this->hasMagicLink) {
         return $this->error(
            'Magic link authentication is not enabled.',
            'feature_disabled',
            400
         );
      }
      $email = sanitize_email($request->get_param('user_email'));
      $type = sanitize_text_field($request->get_param('type') ?? 'login');
      $redirect_to = $request->get_param('redirect_to');
      // Verify Turnstile
      if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
         return $this->error(
            'Security verification failed. Please try again.',
            'turnstile_failed',
            403
         );
      }
      $context = [];
      if ($redirect_to) {
         $context['redirect_to'] = esc_url_raw($redirect_to);
      }
      // Send magic link
      $result = JVB()->magicLink()->sendMagicLink($email, $type, $context);
      if (is_wp_error($result)) {
         $this->logError('Magic link send failed', [
            'email' => $email,
            'type' => $type,
            'error' => $result->get_error_message(),
         ]);
         return $this->error(
            $result->get_error_message(),
            $result->get_error_code(),
            400
         );
      }
      $this->auditLog('magic_link_sent', [
         'email' => $email,
         'type' => $type,
      ]);
      return $this->success([
         'message' => 'Check your email for a magic link to sign in!',
         'title' => 'Success!',
         'description' => [
            'We\'ve sent you an email with a magic link.',
            'Click it to sign in instantly!',
         ]
      ]);
   }
   /**
    * Handle logout
    */
   public function handleLogout(WP_REST_Request $request): WP_REST_Response
   {
@@ -208,460 +483,269 @@
      // Clear session fingerprint
      $this->clearSessionFingerprint($user_id);
      // Audit log
      $this->auditLog('user_logout', ['user_id' => $user_id]);
      // WordPress logout
      wp_logout();
      return $this->success([
         'message' => 'Logged out successfully'
      $this->auditLog('logout', [
         'user_id' => $user_id,
      ]);
      return $this->success([
         'message' => 'Logged out successfully',
         'redirect' => home_url('/login/'),
      ]);
   }
   /************************************************************
    * SESSION FINGERPRINTING
    *
    * Detects session hijacking by validating that session hasn't
    * moved to a different device/network. Uses IP class (not full IP)
    * to allow mobile network changes without breaking sessions.
    ************************************************************/
   /**
    * Store session fingerprint for hijacking detection
    */
   protected function storeSessionFingerprint(int $user_id, WP_REST_Request $request): void
   {
      if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) {
         return;
      }
      $fingerprint = $this->generateSessionFingerprint($request);
      update_user_meta($user_id, BASE . 'session_fingerprint', $fingerprint);
      update_user_meta($user_id, BASE . 'session_timestamp', time());
   }
   /**
    * Get current authentication status
    * Generate session fingerprint for hijacking detection
    */
   public function getAuthStatus(WP_REST_Request $request): WP_REST_Response
   protected function generateSessionFingerprint(WP_REST_Request $request): string
   {
      if (!is_user_logged_in()) {
         return $this->success([
            'authenticated' => false
         ]);
      }
      $user = wp_get_current_user();
      return $this->success([
         'authenticated' => true,
         'user' => $this->formatUserData($user)
      ]);
      return hash('sha256', implode('|', [
         $request->get_header('User-Agent') ?? '',
         // Use IP class instead of full IP to allow for mobile network changes
         $this->getIPClass(
            $request->get_header('X-Forwarded-For')
               ?: $request->get_header('X-Real-IP')
               ?: $_SERVER['REMOTE_ADDR'] ?? ''
         )
      ]));
   }
   /**
    * Request password reset
    * Get IP class (first 3 octets) for session validation
    * Allows for minor IP changes (common with mobile networks)
    */
   public function requestPasswordReset(WP_REST_Request $request): WP_REST_Response
   protected function getIPClass(string $ip): string
   {
      $email = $request->get_param('user_email')??'';
      $email = sanitize_email($email);
      $token = $request->get_param('cf-turnstile-response')??'';
      if (!$this->verifyTurnstile($token)){
         return $this->error('Security verification failed', 'turnstile_failed', 403);
      }
      // Use WordPress's built-in function
      $result = retrieve_password($email);
      // Log but don't expose
      if (is_wp_error($result)) {
         $this->auditLog('password_reset_failed', [
            'email' => $email,
            'reason' => $result->get_error_code()
         ]);
      } else {
         $this->auditLog('password_reset_sent', [
            'email' => $email
         ]);
      }
      return $this->success([
         'message' => 'If an account exists with this email, you will receive a password reset link.'
      ]);
      $parts = explode('.', $ip);
      return implode('.', array_slice($parts, 0, 3));
   }
   /**
    * Complete password reset with token
    * Validate session fingerprint against stored value
    */
   public function resetPassword(WP_REST_Request $request): WP_REST_Response
   protected function validateSessionFingerprint(int $user_id, WP_REST_Request $request): bool
   {
      $key = sanitize_text_field($request->get_param('key'));
      $login = sanitize_text_field($request->get_param('login'));
      $password = $request->get_param('password');
      if (empty($key) || empty($login)) {
         return $this->error('Invalid reset link', 'invalid_key', 400);
      // Only enforce if enabled in config
      if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) {
         return true;
      }
      // Use WordPress's native key verification
      $user = check_password_reset_key($key, $login);
      $stored = get_user_meta($user_id, BASE . 'session_fingerprint', true);
      $current = $this->generateSessionFingerprint($request);
      if (is_wp_error($user)) {
         return $this->error(
            'Invalid or expired reset link',
            'invalid_token',
            400
         );
      if (empty($stored)) {
         // First request - store fingerprint
         update_user_meta($user_id, BASE . 'session_fingerprint', $current);
         update_user_meta($user_id, BASE . 'session_timestamp', time());
         return true;
      }
      // Validate password strength
      $validation = $this->validatePassword($password);
      if (is_wp_error($validation)) {
         return $this->error(
            $validation->get_error_message(),
            'weak_password',
            400
         );
      }
      // Reset the password
      reset_password($user, $password);
      // Log them in
      wp_set_current_user($user->ID);
      wp_set_auth_cookie($user->ID, true);
      // Store session fingerprint
      $this->storeSessionFingerprint($user->ID, $request);
      // Audit log
      $this->auditLog('password_reset_complete', [
         'user_id' => $user->ID
      ]);
      return $this->success([
         'message' => 'Password reset successfully',
         'redirect' => home_url('/dash')
      ]);
      // Compare using timing-safe comparison
      return hash_equals($stored, $current);
   }
   /**
    * Refresh session (extends session duration)
    * Clear session fingerprint (call on logout)
    */
   public function refreshSession(WP_REST_Request $request): WP_REST_Response
   protected function clearSessionFingerprint(int $user_id): void
   {
      $user_id = get_current_user_id();
      // Validate session fingerprint
      if (!$this->validateSessionFingerprint($user_id, $request)) {
         wp_logout();
         return $this->unauthorized('Session validation failed');
      }
      // Refresh auth cookie
      wp_set_auth_cookie($user_id, true);
      return $this->success([
         'message' => 'Session refreshed'
      ]);
      delete_user_meta($user_id, BASE . 'session_fingerprint');
      delete_user_meta($user_id, BASE . 'session_timestamp');
   }
   public function handleRegister(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_json_params();
      // Duplicate submission check
      if (!$this->checkRequestId($data['request_id'] ?? '')) {
         return $this->error('Duplicate request detected', 'duplicate_request', 409);
      }
      // Verify Turnstile
      if (!$this->verifyTurnstile($data['cf-turnstile-response'] ?? '')) {
         return $this->error('Security verification failed', 'turnstile_failed', 403);
      }
      $name = sanitize_text_field($data['name'] ?? '');
      $email = sanitize_email($data['email'] ?? '');
      $user_type = sanitize_text_field($data['user_select'] ?? 'subscriber');
      // Validate fields
      if (empty($name)) {
         return $this->error('Name is required', 'missing_name', 400, 'name');
      }
      if (empty($email)) {
         return $this->error('Email is required', 'missing_email', 400, 'email');
      }
      // Spam prevention
      if ($user_type === 'subscriber' && count(JVB_USER) > 0) {
         $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false);
         if (!empty($registerable)) {
            return $this->error('Please select a valid account type', 'invalid_user_type', 400, 'user_select');
         }
      }
      // Check if role can register
      if ($user_type !== 'subscriber') {
         if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) {
            return $this->error('Invalid account type', 'invalid_user_type', 400, 'user_select');
         }
      }
      // Check if email exists
      if (email_exists($email)) {
         return $this->error('Email already registered', 'duplicate_email', 400, 'email');
      }
      // Allow WP plugins to add registration errors
      $errors = new WP_Error();
      $errors = apply_filters('registration_errors', $errors, $email, $email);
      if ($errors->has_errors()) {
         return $this->error(
            $errors->get_error_message(),
            $errors->get_error_code(),
            400
         );
      }
      // Create user
      $user_id = wp_create_user($email, wp_generate_password(), $email);
      if (is_wp_error($user_id)) {
         return $this->error($user_id->get_error_message(), 'user_creation_failed', 500);
      }
      // Update user data
      wp_update_user([
         'ID' => $user_id,
         'display_name' => $name,
         'first_name' => strtok($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);
         }
      }
      if (Features::forUser($user_type)->has('namedDirectory')) {
         $upload_dir = wp_upload_dir();
         $user_directory = $user_type.'/'.$user_id;
         $target_dir = $upload_dir['basedir'].'/'.$user_directory;
         wp_mkdir_p($target_dir);
      }
      // Save additional fields
      update_user_meta($user_id, BASE . 'user_type', $user_type);
      // Process additional fields from form
      foreach ($data as $key => $value) {
         if (in_array($key, ['name', 'email', 'action', 'request_id', 'user_select', 'cf-turnstile-response'])) {
            continue;
         }
         update_user_meta($user_id, BASE . $key, sanitize_text_field($value));
      }
      // Handle token handlers
      do_action('jvbUserRegistered', $user_id, $email, $data);
      return $this->success([
         'message' => 'Registration successful! Check your email.',
         'user_id' => $user_id
      ]);
   }
   /**************************************************************
      HELPERS
    * HELPERS
    **************************************************************/
   /**
    * Format user data for response
    */
   protected function formatUserData(WP_User $user): array
   {
      return [
         'id' => $user->ID,
         'username' => $user->user_login,
         'email' => $user->user_email,
         'display_name' => $user->display_name,
         'roles' => $user->roles,
         'capabilities' => [
            'manage_options' => user_can($user, 'manage_options'),
            'skip_moderation' => user_can($user, 'skip_moderation'),
            // Add other relevant capabilities
         ]
      ];
   }
   /**
    * Get login redirect URL based on user role
    * Get redirect URL after login
    */
   protected function getLoginRedirect(WP_User $user): string
   protected function getRedirectUrl(WP_User $user, ?string $redirect_to = null): string
   {
      // Use provided redirect if safe
      if ($redirect_to && wp_validate_redirect($redirect_to, false)) {
         return esc_url_raw($redirect_to);
      }
      // Default redirect based on user capability
      if (user_can($user, 'manage_options')) {
         return admin_url();
      }
      // Redirect to dashboard for members
      if (function_exists('isOurPeople') && isOurPeople()) {
         return home_url('/dash');
      if (isOurPeople($user->ID)) {
         return home_url('/dash/');
      }
      return home_url();
   }
   /**
    * Validate password strength
    * Get user-friendly login error message
    */
   protected function validatePassword(string $password): bool|WP_Error
   protected function getLoginErrorMessage(WP_Error $error): string
   {
      if (strlen($password) < 8) {
         return new WP_Error(
            'weak_password',
            'Password must be at least 8 characters long'
         );
      }
      $code = $error->get_error_code();
      // Add additional strength requirements as needed
      // - Must contain uppercase
      // - Must contain number
      // - Must contain special character
      return true;
   }
   /**
    * Check if account is locked out due to failed attempts
    */
   protected function checkAccountLockout(string $username): bool|WP_Error
   {
      if (!defined('JVB_MAX_LOGIN_ATTEMPTS')) {
         return true;
      }
      $cache_key = 'login_attempts_' . md5($username);
      $attempts = $this->cache->get($cache_key);
      if (!$attempts || !isset($attempts['count'])) {
         return true;
      }
      if ($attempts['count'] >= JVB_MAX_LOGIN_ATTEMPTS) {
         $lockout_duration = defined('JVB_LOCKOUT_DURATION')
            ? JVB_LOCKOUT_DURATION
            : 15 * MINUTE_IN_SECONDS;
         $time_remaining = $lockout_duration - (time() - $attempts['timestamp']);
         if ($time_remaining > 0) {
            return new WP_Error(
               'account_locked',
               sprintf(
                  'Too many failed login attempts. Please try again in %d minutes.',
                  ceil($time_remaining / 60)
               )
            );
         }
         // Lockout expired - clear attempts
         $this->cache->delete($cache_key);
         return true;
      }
      return true;
   }
   /**
    * Track failed login attempt
    */
   protected function trackFailedLogin(string $username): void
   {
      $cache_key = 'login_attempts_' . md5($username);
      $attempts = $this->cache->get($cache_key) ?: [
         'count' => 0,
         'timestamp' => time()
      $messages = [
         'invalid_email' => 'Invalid email address.',
         'invalid_username' => 'Invalid email address.',
         'incorrect_password' => 'Incorrect password.',
         'empty_password' => 'Please enter your password.',
         'empty_username' => 'Please enter your email address.',
      ];
      $attempts['count']++;
      $attempts['timestamp'] = time();
      $lockout_duration = defined('JVB_LOCKOUT_DURATION')
         ? JVB_LOCKOUT_DURATION
         : 15 * MINUTE_IN_SECONDS;
      $this->cache->set($cache_key, $attempts, $lockout_duration);
      // Audit log
      $this->auditLog('failed_login', [
         'username' => $username,
         'attempts' => $attempts['count']
      ]);
      return $messages[$code] ?? 'Login failed. Please check your credentials.';
   }
   /**
    * Clear failed login attempts on successful login
    * Validate user role selection during registration
    */
   protected function clearFailedAttempts(string $username): void
   protected function validateUserRole(string $user_select): string|WP_Error
   {
      $cache_key = 'login_attempts_' . md5($username);
      $this->cache->delete($cache_key);
   }
   public function checkEmailExists(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_json_params();
      $email = sanitize_email($data['email'] ?? '');
      return $this->success([
         'exists' => is_email($email) && email_exists($email)
      ]);
   }
   /***********************************************************************
    * HELPER METHODS
    ***********************************************************************/
   protected function checkRequestId(string $request_id): bool
   {
      if (empty($request_id)) {
         return true;
      // Default to subscriber
      if (empty($user_select) || $user_select === 'subscriber') {
         return 'subscriber';
      }
      $cache_key = 'request_' . $request_id;
      if (get_transient($cache_key)) {
         return false;
      // Check if role is valid and can register
      $registrar = Registrar::getInstance($user_select);
      if (!$registrar) {
         return new WP_Error('invalid_role', 'Invalid role selected.');
      }
      set_transient($cache_key, true, 60);
      return true;
   }
   protected function verifyTurnstile(string $token): bool
   {
      if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
         return true;
      if (!($registrar->hasFeature('can_register') ?? false)) {
         return new WP_Error('role_not_allowed', 'This role cannot be selected during registration.');
      }
      if (empty($token)) {
         return false;
      }
      return JVB()->connect('cloudflare')->verifyTurnstile($token);
      return $registrar->getBased();
   }
   /**
    * Helper to return error response
    * Process referral code during registration
    */
   protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response
   protected function processReferralCode(int $user_id, string $referral_code): void
   {
      if ($this->requestId) {
         delete_transient('request_'.$this->requestId);
         $this->requestId = null;
      if (!Site::has('referrals')) {
         return;
      }
      return parent::error($message, $code, $status, $field);
      try {
         JVB()->referrals()->processReferralCode($user_id, $referral_code);
      } catch (\Exception $e) {
         $this->logError('Referral processing failed', [
            'user_id' => $user_id,
            'code' => $referral_code,
            'error' => $e->getMessage(),
         ], 'warning');
      }
   }
   /**
    * Process additional registration fields
    */
   protected function processRegistrationFields(int $user_id, array $data): void
   {
      // Get registration form configuration
      $form_fields = get_option(BASE . 'registration_form_fields', []);
      foreach ($form_fields as $field_name => $field_config) {
         // Skip system fields
         if (in_array($field_name, ['user_name', 'user_email', 'user_select', 'referral_code'])) {
            continue;
         }
         // Save field value if present
         if (isset($data[$field_name]) && !empty($data[$field_name])) {
            $value = $data[$field_name];
            // Sanitize based on field type
            if (isset($field_config['type'])) {
               $value = $this->sanitizeFieldValue($value, $field_config['type']);
            }
            update_user_meta($user_id, BASE . $field_name, $value);
         }
      }
   }
   /**
    * Sanitize field value based on type
    */
   protected function sanitizeFieldValue(mixed $value, string $type):string|int
   {
      switch ($type) {
         case 'email':
            return sanitize_email($value);
         case 'url':
            return esc_url_raw($value);
         case 'textarea':
            return sanitize_textarea_field($value);
         case 'number':
            return absint($value);
         default:
            return sanitize_text_field($value);
      }
   }
   protected function buildAuth(?int $user = null): array
   {
      if (is_user_logged_in()) {
         $user = ($user) ?: get_current_user_id();
         return [
            'authenticated' => true,
            'user' => $user,
            'nonces' => $this->getUserNonces($user)
         ];
      }
      return [
         'authenticated' => false,
         'user' => false,
         'nonces' => [
            'wp_rest' => wp_create_nonce('wp_rest')
         ]
      ];
   }
   protected function getUserNonces(int $userID):array {
      $nonces = [
         'wp_rest'   => wp_create_nonce('wp_rest'),
      ];
      if (Site::has('dashboard')) {
         $nonces['dash'] = wp_create_nonce('dash-'.$userID);
      }
      if (Site::has('favourites')) {
         $nonces['favourites'] = wp_create_nonce('favourites-'.$userID);
      }
      if (!empty(Registrar::getFeatured('karma'))) {
         $nonces['votes'] = wp_create_nonce('votes-'.$userID);
      }
      if (Site::has('notifications')) {
         $nonces['notifications'] = wp_create_nonce('notifications-'.$userID);
      }
      return $nonces;
   }
}