cache = Cache::for('magic_links', $this->token_expiry); $this->referral_cache = Cache::for('referral_magic_links', 14 * DAY_IN_SECONDS); // Hook into WordPress auth flow add_action('template_redirect', [$this, 'handleMagicLinkClick']); add_action('wp_login_failed', [$this, 'handleFailedLogin']); add_action('jvb_process_login_tokens', [$this, 'processRegistrationToken'], 10, 3); } /** * Generate and send a magic link * * @param string $email User's email address * @param string $type Type of magic link (login, signup, referral, reset) * @param array $context Additional context (referral_code, redirect_url, etc.) * @return true|WP_Error */ public function sendMagicLink(string $email, string $type = self::TYPE_LOGIN, array $context = []) { // Validate email $email = sanitize_email($email); if (!is_email($email)) { return new WP_Error('invalid_email', 'Invalid email address'); } // Check rate limiting $rate_check = $this->checkRateLimit($email); if (is_wp_error($rate_check)) { return $rate_check; } // Handle different link types switch ($type) { case self::TYPE_LOGIN: return $this->sendLoginLink($email, $context); case self::TYPE_SIGNUP: return $this->sendSignupLink($email, $context); case self::TYPE_REFERRAL: return $this->sendReferralLink($email, $context); case self::TYPE_RESET: return $this->sendResetLink($email, $context); default: return new WP_Error('invalid_type', 'Invalid magic link type'); } } /** * Generate a secure token */ protected function generateToken(string $email, string $type, array $data = []): string { $token = wp_generate_password(32, false); $token_data = array_merge([ 'email' => $email, 'type' => $type, 'created' => time() ], $data); // Use longer expiry for referral tokens if ($type === self::TYPE_REFERRAL) { $this->referral_cache->set($token, $token_data); } else { $this->cache->set($token, $token_data); } return $token; } /** * Verify a token */ public function verifyToken(string $token, string $email): array|WP_Error { // Try regular cache first, then referral cache $token_data = $this->cache->get($token); if (!$token_data) { $token_data = $this->referral_cache->get($token); } if (!$token_data) { error_log('Token not found. Checking cache stats...'); return new WP_Error('invalid_token', 'Invalid or expired token'); } if ($token_data['email'] !== $email) { return new WP_Error('email_mismatch', 'Token does not match email'); } // Delete token after verification (single use) // Check which cache it's in and delete from the correct one if ($token_data['type'] === 'referral') { $this->referral_cache->forget($token); } else { $this->cache->forget($token); } return $token_data; } /** * Check rate limiting for sending magic links */ protected function checkRateLimit(string $email): bool|WP_Error { $cache_key = 'rate_limit_' . md5($email); $attempts = $this->cache->get($cache_key); if (!$attempts) { $attempts = ['count' => 0, 'timestamp' => time()]; } // Reset counter if window has passed if (time() - $attempts['timestamp'] > $this->rate_limit_window) { $attempts = ['count' => 0, 'timestamp' => time()]; } // Check if limit exceeded if ($attempts['count'] >= $this->max_attempts_per_hour) { return new WP_Error( 'rate_limit_exceeded', 'Too many magic link requests. Please try again in an hour.' ); } // Increment counter $attempts['count']++; $this->cache->set($cache_key, $attempts, $this->rate_limit_window); return true; } /** * Send login magic link to existing user */ protected function sendLoginLink(string $email, array $context): bool|WP_Error { $user = get_user_by('email', $email); if (!$user) { return new WP_Error('user_not_found', 'No account found with this email'); } $token = $this->generateToken($email, self::TYPE_LOGIN, [ 'user_id' => $user->ID ]); $magic_url = add_query_arg([ 'magic_token' => $token, 'email' => rawurlencode($email), 'action' => 'magic_login' ], home_url('/')); if (!empty($context['redirect_to'])) { $magic_url = add_query_arg('redirect_to', urlencode($context['redirect_to']), $magic_url); } $subject = 'Sign in to ' . get_bloginfo('name'); $message = $this->getLoginEmailTemplate($user->display_name, $magic_url); $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Log in to '. get_bloginfo('name')); return $sent ? true : new WP_Error('email_failed', 'Failed to send magic link'); } /** * Send signup magic link for new user registration */ protected function sendSignupLink(string $email, array $context): bool|WP_Error { // Check if user already exists if (email_exists($email)) { return $this->sendLoginLink($email, $context); } $token_data = [ 'name' => $context['name'] ?? '', 'role' => $context['role'] ?? 'subscriber', 'meta' => $context['meta'] ?? [] ]; $token = $this->generateToken($email, self::TYPE_SIGNUP, $token_data); $magic_url = add_query_arg([ 'magic_token' => $token, 'email' => rawurlencode($email), 'action' => 'magic_signup' ], home_url('/')); $subject = 'Complete your ' . get_bloginfo('name') . ' registration'; $message = $this->getSignupEmailTemplate($context['name'] ?? '', $magic_url); $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Complete Registration'); return $sent ? true : new WP_Error('email_failed', 'Failed to send signup link'); } /** * Send referral signup link */ protected function sendReferralLink(string $email, array $context): bool|WP_Error { if (empty($context['referral_code'])) { return new WP_Error('missing_referral', 'Referral code is required'); } $token_data = [ 'referral_code' => $context['referral_code'], 'name' => $context['name'] ?? '', 'role' => $context['role'] ?? 'subscriber', 'email' => $email ]; $token = $this->generateToken($email, self::TYPE_REFERRAL, $token_data); $magic_url = add_query_arg([ 'magic_token' => $token, 'email' => rawurlencode($email), 'action' => 'magic_referral' ], home_url('/')); $referrer_name = $context['referrer_name'] ?? 'A friend'; $reward_text = $context['reward_text'] ?? ''; $subject = (array_key_exists('subject', $context) && $context['subject'] !== '') ? $context['subject'] : $referrer_name . ' invited you to join ' . get_bloginfo('name'); $message = $this->getReferralEmailTemplate($context['name'] ?? '', $referrer_name, $magic_url, $reward_text, $context); $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Accept Invitation'); return $sent ? true : new WP_Error('email_failed', 'Failed to send referral link'); } /** * Send password reset magic link */ protected function sendResetLink(string $email, array $context): bool|WP_Error { $user = get_user_by('email', $email); if (!$user) { return new WP_Error('user_not_found', 'No account found with this email'); } $token = $this->generateToken($email, self::TYPE_RESET, [ 'user_id' => $user->ID ]); $magic_url = add_query_arg([ 'magic_token' => $token, 'email' => rawurlencode($email), 'action' => 'magic_reset' ], home_url('/')); $subject = 'Reset your password'; $message = $this->getResetEmailTemplate($user->display_name, $magic_url); $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Reset Password'); return $sent ? true : new WP_Error('email_failed', 'Failed to send reset link'); } /** * Handle magic link click */ public function handleMagicLinkClick(): void { if (!isset($_GET['action']) || !isset($_GET['magic_token']) || !isset($_GET['email'])) { return; } $action = sanitize_text_field($_GET['action']); $token = sanitize_text_field($_GET['magic_token']); $email = sanitize_email(rawurldecode($_GET['email'])); if (!in_array($action, ['magic_login', 'magic_signup', 'magic_referral', 'magic_reset'])) { return; } $token_data = $this->verifyToken($token, $email); if (is_wp_error($token_data)) { $this->handleInvalidToken($token_data); return; } switch ($action) { case 'magic_login': $this->processLogin($token_data); break; case 'magic_signup': $this->processSignup($token_data); break; case 'magic_referral': $this->processReferralSignup($token_data); break; case 'magic_reset': $this->processPasswordReset($token_data); break; } } /** * Process login via magic link */ protected function processLogin(array $token_data): void { $user = get_user_by('ID', $token_data['user_id']); if (!$user) { wp_die('Invalid user'); } wp_clear_auth_cookie(); wp_set_current_user($user->ID); wp_set_auth_cookie($user->ID, true); do_action('wp_login', $user->user_login, $user); $redirect = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : home_url('/dash'); wp_safe_redirect($redirect); exit; } /** * Process signup via magic link */ protected function processSignup(array $token_data): void { if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) { JVB()->error()->log('[MagicLinkManager]Could not process Signup'); return; } $user_id = wp_create_user( $token_data['email'], wp_generate_password(20, true, true), $token_data['email'] ); if (is_wp_error($user_id)) { wp_die('Failed to create account: ' . $user_id->get_error_message()); } $user = get_user_by('ID', $user_id); $user->set_role($token_data['role']); if (!empty($token_data['name'])) { wp_update_user([ 'ID' => $user_id, 'display_name' => $token_data['name'], 'first_name' => $token_data['name'] ]); } if (!empty($token_data['meta'])) { foreach ($token_data['meta'] as $key => $value) { update_user_meta($user_id, BASE . $key, $value); } } wp_set_current_user($user_id); wp_set_auth_cookie($user_id, true); do_action('user_register', $user_id); do_action('wp_login', $user->user_login, $user); wp_safe_redirect(home_url('/dash?welcome=1')); exit; } /** * Process referral signup via magic link */ /** * Process referral signup via magic link */ protected function processReferralSignup(array $token_data): void { if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) { JVB()->error()->log('[MagicLinkManager]Could not process Referral Signup'); return; } $email = sanitize_email($token_data['email']); if (email_exists($email)) { wp_die('Looks like you already have an account!'); } $role = JVB()->referrals()->getRole(); $pass = wp_generate_password(20, true, true); $name = sanitize_text_field($token_data['name']); $user_id = wp_insert_user([ 'user_login' => $email, 'user_email' => $email, 'user_pass' => $pass, 'display_name' => $name, 'role' => $role ]); if (!is_wp_error($user_id)) { $response = JVB()->routes('login')->login($email, $pass, true); if ($response) { wp_safe_redirect(home_url('/dash?welcome=1&referral=1')); exit; } } else { JVB()->error()->log( '[MagicLinkManager]', $user_id->get_error_message(), $token_data ); } } /** * Process password reset via magic link */ protected function processPasswordReset(array $token_data): void { $user = get_user_by('ID', $token_data['user_id']); if (!$user) { wp_die('Invalid user'); } // Log user in and redirect to password change page wp_set_current_user($user->ID); wp_set_auth_cookie($user->ID, true); wp_safe_redirect(admin_url('profile.php?password_reset=1')); exit; } /** * Handle invalid token */ protected function handleInvalidToken(WP_Error $error): void { wp_die($error->get_error_message()); } /** * Handle failed login - offer magic link option */ public function handleFailedLogin(string $username): void { // Could add logic here to automatically offer magic link // after multiple failed attempts } /** * Optionally block standard password auth for magic-link-only users */ public function blockStandardAuth($user, $username, $password) { if ($user instanceof WP_User) { $magic_only = get_user_meta($user->ID, BASE . 'magic_link_only', true); if ($magic_only) { return new WP_Error('magic_link_required', 'Please use the login link sent to your email'); } } return $user; } // ======================================== // EMAIL TEMPLATES // ======================================== protected function getLoginEmailTemplate(string $name, string $magic_url): string { $content = JVB()->email()->h2('Hey ' . esc_html($name) . '!'); $content .= '
Click the button below to sign in to your account instantly - no password needed!
'; $content .= JVB()->email()->button($magic_url, 'Sign In Now'); $content .= 'Or copy and paste this link into your browser:
'; $content .= JVB()->email()->link($magic_url); $content .= JVB()->email()->divider(); $content .= 'If you didn\'t request this, you can safely ignore this email. This link expires in 15 minutes.
'; return $content; } protected function getSignupEmailTemplate(string $name, string $magic_url): string { $content = JVB()->email()->h2('Welcome' . ($name ? ', ' . esc_html($name) : '') . '!'); $content .= 'Click the button below to complete your registration and access your account!
'; $content .= JVB()->email()->button($magic_url, 'Complete Registration'); $content .= 'Or copy and paste this link:
'; $content .= JVB()->email()->link($magic_url); $content .= JVB()->email()->spacer(10); $content .= 'This link expires in 24 hours.
'; return $content; } protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text, array $context): string { $content = JVB()->email()->h2('Hey' . ($name ? ' ' . esc_html($name) : '') . '!'); $content .= sprintf( '%s thinks you\'d love %s!
', esc_html($referrer_name), esc_html(get_bloginfo('name')) ); if (!empty($context['message'])) { $content .= JVB()->email()->callout(nl2br(esc_html($context['message']))); } if ($reward_text) { $content .= JVB()->email()->alert( 'Special Welcome Offer: ' . esc_html($reward_text), 'success' ); } $content .= JVB()->email()->button($magic_url, 'Join Now'); $content .= 'Or copy and paste this link:
'; $content .= JVB()->email()->link($magic_url); $content .= JVB()->email()->spacer(10); $content .= 'This invitation expires in 14 days.
'; return $content; } }