Jake Vanderwerf
2026-01-19 0113d2e9c9ff34a6ffb10707cc76d34b67a0c367
inc/rest/routes/LoginRoutes.php
@@ -1,14 +1,12 @@
<?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 WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WP_Session_Tokens;
use WP_User;
if (!defined('ABSPATH')) {
@@ -17,19 +15,12 @@
class LoginRoutes extends RestRouteManager
{
   protected EmailManager $emailManager;
   protected MagicLinkManager $magic_link;
   protected LoginManager $loginManager;
   protected ?string $requestId = null;
   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();
      }
      parent::__construct();
   }
@@ -113,7 +104,7 @@
      ]);
      register_rest_route($this->namespace, '/auth/register', [
         'method' => 'POST',
         'methods'   => 'POST',
         'callback'  => [$this, 'handleRegister'],
         'permission_callback' => [$this, 'checkRateLimit'],
      ]);
@@ -134,14 +125,15 @@
   public function handleLogin(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_json_params();
      // Verify Turnstile
      if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
      if (!$this->verifyTurnstile($data['cf-turnstile-response'] ?? '')) {
         return $this->error('Security verification failed', 'turnstile_failed', 403);
      }
      $username = $request->get_param('user_email');
      $password = $request->get_param('user_password');
      $remember = (bool)$request->get_param('remember_me');
      $username = sanitize_email($data['user_email'] ?? '');
      $password = $data['user_password'] ?? '';
      $remember = (bool)($data['remember_me'] ?? false);
      // Check for account lockout
      $lockout = $this->checkAccountLockout($username);
@@ -152,35 +144,42 @@
            429
         );
      }
      return $this->login($username, $password, $remember, $request);
   }
   public function login(string $username, string $password, bool $remember, ?WP_REST_Request $request = null):WP_REST_Response|bool
   {
      // Attempt login
      $user = wp_signon([
         'user_login'   => $username,
         'user_email'   => $username,
         'user_password' => $password,
         'remember' => $remember
      ], false);
      ], is_ssl());
      if (is_wp_error($user)) {
         // Track failed attempt
         $this->trackFailedLogin($username);
         return $this->error(
         return ($request) ? $this->error(
            'Invalid username or password',
            'login_failed',
            401
         );
         ) : false;
      }
      // Clear failed attempts on success
      $this->clearFailedAttempts($username);
      // Set auth cookie with remember me flag
      wp_set_current_user($user->ID);
      wp_set_auth_cookie($user->ID, $remember);
      wp_set_auth_cookie($user->ID, $remember, is_ssl());
      // Store session fingerprint for hijacking protection
      $this->storeSessionFingerprint($user->ID, $request);
      if ($request) {
         $this->storeSessionFingerprint($user->ID, $request);
      }
      // Trigger WordPress login action
      do_action('wp_login', $user->user_login, $user);
@@ -191,11 +190,33 @@
         'remember' => $remember
      ]);
      return $this->success([
      return ($request) ? $this->success([
         'message' => 'Login successful',
         'user' => $this->formatUserData($user),
         'redirect' => $this->getLoginRedirect($user)
      ]);
         'redirect' => $this->getRedirect($user, $request->get_param('redirect_to')),
         'auth' => $this->buildAuth($user->ID)
      ]) : true;
   }
   protected function getUserNonces(int $userID):array {
      $nonces = [
         'wp_rest'   => wp_create_nonce('wp_rest'),
      ];
      if (Features::forSite()->has('dashboard')) {
         $nonces['dash'] = wp_create_nonce('dash-'.$userID);
      }
      if (Features::forSite()->has('favourites')) {
         $nonces['favourites'] = wp_create_nonce('favourites-'.$userID);
      }
      if (Features::anyContentHas('karma') ||
         Features::anyTaxonomyHas('karma') ||
         Features::anyUserHas('karma')) {
         $nonces['votes'] = wp_create_nonce('votes-'.$userID);
      }
      if (Features::forSite()->has('notifications')) {
         $nonces['notifications'] = wp_create_nonce('notifications-'.$userID);
      }
      return $nonces;
   }
   /**
@@ -215,27 +236,64 @@
      wp_logout();
      return $this->success([
         'message' => 'Logged out successfully'
         'message' => 'Logged out successfully',
         'redirect' => $this->getRedirect(get_userdata($user_id), $request->get_param('redirect_to'), 'logout')
      ]);
   }
   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),
            'session_id' => $this->getSessionId($user)
         ];
      }
      return [
         'authenticated' => false,
         'currentUser' => false,
         'nonces' => [
            'wp_rest' => wp_create_nonce('wp_rest')
         ],
         'session_id' => null
      ];
   }
   /**
    * Get unique session identifier that changes on login/logout
    */
   protected function getSessionId(int $user_id): string
   {
      $token = wp_get_session_token(); // Current session token
      if (!$token) {
         // Fallback to a hash based on user ID and current timestamp
         // This will be replaced once the session token is available
         return md5($user_id . time());
      }
      return md5($token);
   }
   /**
    * Get current authentication status
    */
   public function getAuthStatus(WP_REST_Request $request): WP_REST_Response
   {
      if (!is_user_logged_in()) {
         return $this->success([
            'authenticated' => false
         ]);
      }
      $user = wp_get_current_user();
      $responseData = $this->buildAuth();
      return $this->success([
         'authenticated' => true,
         'user' => $this->formatUserData($user)
      ]);
      $response = $this->success($responseData);
      // Add caching headers
      $response->header('Cache-Control', 'private, max-age=300'); // 5 minutes
      $response->header('Vary', 'Cookie'); // Important for nginx
      return $response;
   }
   /**
@@ -312,6 +370,9 @@
      wp_set_current_user($user->ID);
      wp_set_auth_cookie($user->ID, true);
      if (session_status() === PHP_SESSION_ACTIVE) {
         session_regenerate_id(true);
      }
      // Store session fingerprint
      $this->storeSessionFingerprint($user->ID, $request);
@@ -364,6 +425,7 @@
      $name = sanitize_text_field($data['name'] ?? '');
      $email = sanitize_email($data['email'] ?? '');
      $referral_code = $request->get_param('referral_code')??'';
      $user_type = sanitize_text_field($data['user_select'] ?? 'subscriber');
      // Validate fields
@@ -375,14 +437,6 @@
         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'])) {
@@ -395,6 +449,19 @@
         return $this->error('Email already registered', 'duplicate_email', 400, 'email');
      }
      // Validate referral code if provided
      $referrer_id = null;
      if ($referral_code) {
         $code = strtoupper(sanitize_text_field($referral_code));
         $referrer = JVB()->referrals()->getUserByReferralCode($code);
         if (!$referrer) {
            return $this->error('Invalid referral code', 'invalid_code', 400);
         }
         $referrer_id = $referrer->ID;
      }
      // Allow WP plugins to add registration errors
      $errors = new WP_Error();
      $errors = apply_filters('registration_errors', $errors, $email, $email);
@@ -407,27 +474,42 @@
         );
      }
      // Create user
      $user_id = wp_create_user($email, wp_generate_password(), $email);
      // Update user data
      $role = ($referrer_id) ? get_option(BASE . 'referral_role', BASE . 'client') : jvbCheckBase($user_type);
      $userData = [
         'user_login'   => $email,
         'user_email'   => $email,
         'display_name' => $name,
         'first_name'   => strtok($name, ' '),
         'role'         => $role
      ];
      if (is_wp_error($user_id)) {
         return $this->error($user_id->get_error_message(), 'user_creation_failed', 500);
      // Add password if provided, otherwise generate one
      $password = $request->get_param('password');
      if ($password) {
         $userData['user_pass'] = $password;
      } else {
         $userData['user_pass'] = wp_generate_password(20, true, true);
      }
      // Update user data
      wp_update_user([
         'ID' => $user_id,
         'display_name' => $name,
         'first_name' => strtok($name,' ')
      ]);
      $user_id = wp_insert_user($userData);
      if (is_wp_error($user_id)) {
         return $this->error(
            $user_id->get_error_message(),
            'registration_failed',
            500
         );
      }
      // Process referral if code was provided
      if ($referrer_id) {
         update_user_meta($user_id, BASE . 'pending_referral_code', $referral_code);
      }
      // 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);
      $user = get_userdata($user_id);
      if ($user_type !== 'subscriber') {
         // Check if needs approval
         if (Features::forMembership()->has('memberVerified') &&
@@ -445,9 +527,6 @@
         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'])) {
@@ -456,13 +535,31 @@
         update_user_meta($user_id, BASE . $key, sanitize_text_field($value));
      }
      $redirect = $this->getRedirect($user, $request->get_param('redirect_to')??get_home_url(null,'/dash'), 'register');
      // Handle token handlers
      do_action('jvbUserRegistered', $user_id, $email, $data);
      $magic_link_result = JVB()->magicLink()?->sendMagicLink(
         $email,
         'login',
         [
            'user_id' => $user_id,
            'redirect' => $redirect
         ]
      );
      if (is_wp_error($magic_link_result)) {
         return $this->error(
            'Account created but failed to send verification email. Please use password reset.',
            'magic_link_failed',
            500
         );
      }
      return $this->success([
         'message' => 'Registration successful! Check your email.',
         'user_id' => $user_id
         'user_id' => $user_id,
         'redirect' => $redirect
      ]);
   }
   /**************************************************************
@@ -487,18 +584,28 @@
      ];
   }
   /**
    * Get login redirect URL based on user role
    */
   protected function getLoginRedirect(WP_User $user): string
   protected function getRedirect(WP_User $user, string $url, string $context = 'login'):string
   {
      if (!empty($url)) {
         $url = sanitize_url($url);
         if (wp_validate_redirect($url)) {
            return $url;
         }
      }
      // Redirect to custom dashboard for members
      if (function_exists('isOurPeople') && isOurPeople()) {
         return home_url('/dash');
      }
      // Admins can go to wp-admin if they want (but only if not using custom dashboard)
      if (user_can($user, 'manage_options')) {
         return admin_url();
      }
      // Redirect to dashboard for members
      if (function_exists('isOurPeople') && isOurPeople()) {
         return home_url('/dash');
      $custom_redirect = get_option(BASE . 'after_'.$context.'_redirect');
      if ($custom_redirect) {
         return $custom_redirect;
      }
      return home_url();
@@ -638,18 +745,6 @@
      return true;
   }
   protected function verifyTurnstile(string $token): bool
   {
      if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
         return true;
      }
      if (empty($token)) {
         return false;
      }
      return JVB()->connect('cloudflare')->verifyTurnstile($token);
   }
   /**
    * Helper to return error response