| | |
| | | <?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')) { |
| | |
| | | |
| | | 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(); |
| | | } |
| | |
| | | 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 |
| | | ] |
| | | ] |
| | | 'permission_callback' => [$this, 'checkRateLimit'] |
| | | ]); |
| | | |
| | | // Logout endpoint |
| | |
| | | ]); |
| | | |
| | | register_rest_route($this->namespace, '/auth/register', [ |
| | | 'method' => 'POST', |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleRegister'], |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | ]); |
| | |
| | | |
| | | public function handleLogin(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $data = $request->get_json_params(); |
| | | $data = $request->get_params(); |
| | | error_log('Data: '.print_r($data, true)); |
| | | // Verify Turnstile |
| | | if (!$this->verifyTurnstile($data['cf-turnstile-response'] ?? '')) { |
| | | return $this->error('Security verification failed', 'turnstile_failed', 403); |
| | |
| | | 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); |
| | |
| | | '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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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); |
| | | |
| | |
| | | |
| | | $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 |
| | |
| | | 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('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); |
| | |
| | | ); |
| | | } |
| | | |
| | | // 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') && |
| | |
| | | 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'])) { |
| | |
| | | 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 |
| | | ]); |
| | | } |
| | | /************************************************************** |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Get login redirect URL based on user role |
| | | */ |
| | | protected function getLoginRedirect(WP_User $user): string |
| | | protected function getRedirect(WP_User $user, ?string $url=null, 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(); |