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