From 48721c85ebcfa973ee81719d2467ca80e4253dc9 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 01 May 2026 17:30:03 +0000
Subject: [PATCH] =Edmonton Ink hard test begins! Real testing of the managers and reset routes will commence. So far, just ensuring our classes are all loaded correctly: Site() and its sub-classes Membership, Login, etc. Care should be taken to load conditionally on 'init', as we finish defining most settings by 'plugins_loaded' at priority 5

---
 inc/rest/routes/LoginRoutes.php | 1138 ++++++++++++++++++++++++++++++++---------------------------
 1 files changed, 611 insertions(+), 527 deletions(-)

diff --git a/inc/rest/routes/LoginRoutes.php b/inc/rest/routes/LoginRoutes.php
index ec82f0d..0ef1550 100644
--- a/inc/rest/routes/LoginRoutes.php
+++ b/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;
+	}
 }

--
Gitblit v1.10.0