cache = new CacheManager('magic_links', $this->token_expiry); $this->email = new EmailManager(); // Hook into WordPress auth flow add_action('template_redirect', [$this, 'handleMagicLinkClick']); add_action('wp_login_failed', [$this, 'handleFailedLogin']); // Add magic link option to login page add_action('login_form', [$this, 'addMagicLinkOption']); add_filter('authenticate', [$this, 'blockStandardAuth'], 30, 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'); } } /** * Send login magic link to existing user */ protected function sendLoginLink(string $email, array $context): bool|WP_Error { // Check if user exists $user = get_user_by('email', $email); if (!$user) { return new WP_Error('user_not_found', 'No account found with this email'); } // Generate token $token = $this->generateToken($email, self::TYPE_LOGIN, [ 'user_id' => $user->ID ]); // Build magic link URL $magic_url = add_query_arg([ 'magic_token' => $token, 'email' => urlencode($email), 'action' => 'magic_login' ], home_url('/')); // Add redirect if specified if (!empty($context['redirect_to'])) { $magic_url = add_query_arg('redirect_to', urlencode($context['redirect_to']), $magic_url); } // Send email $subject = 'Sign in to ' . get_bloginfo('name'); $message = $this->getLoginEmailTemplate($user->display_name, $magic_url); $sent = $this->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); } // Generate token with signup data $token_data = [ 'name' => $context['name'] ?? '', 'role' => $context['role'] ?? 'subscriber', 'meta' => $context['meta'] ?? [] ]; $token = $this->generateToken($email, self::TYPE_SIGNUP, $token_data); // Build signup completion URL $magic_url = add_query_arg([ 'magic_token' => $token, 'email' => urlencode($email), 'action' => 'magic_signup' ], home_url('/')); // Send welcome email $subject = 'Complete your ' . get_bloginfo('name') . ' registration'; $message = $this->getSignupEmailTemplate($context['name'] ?? '', $magic_url); $sent = $this->email->sendEmail($email, $subject, $message, 'Confirm Your Account'); 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 { // Check if user already exists if (email_exists($email)) { return new WP_Error('user_exists', 'This person already has an account'); } // Validate referral code if (empty($context['referral_code'])) { return new WP_Error('missing_referral_code', 'Referral code is required'); } // Get referrer info for personalized email $referrer_name = $context['referrer_name'] ?? 'A friend'; // Generate token with referral context $token_data = [ 'name' => $context['name'] ?? '', 'referral_code' => $context['referral_code'], 'referrer_id' => $context['referrer_id'] ?? 0 ]; $token = $this->generateToken($email, self::TYPE_REFERRAL, $token_data); // Build referral signup URL $magic_url = add_query_arg([ 'magic_token' => $token, 'email' => urlencode($email), 'action' => 'magic_referral' ], home_url('/')); // Send personalized referral email $subject = $referrer_name . ' invited you to ' . get_bloginfo('name'); $message = $this->getReferralEmailTemplate( $context['name'] ?? '', $referrer_name, $magic_url, $context['reward_text'] ?? '' ); $sent = $this->email->sendEmail($email, $subject, $message, $referrer_name.' invites you to see the difference at Legacy'); return $sent ? true : new WP_Error('email_failed', 'Failed to send referral invitation'); } /** * Send password reset magic link */ protected function sendResetLink(string $email, array $context):bool|WP_Error { $user = get_user_by('email', $email); if (!$user) { // Return success even if user doesn't exist (security best practice) return true; } $token = $this->generateToken($email, self::TYPE_RESET, [ 'user_id' => $user->ID ]); $magic_url = add_query_arg([ 'magic_token' => $token, 'email' => urlencode($email), 'action' => 'magic_reset' ], home_url('/')); $subject = 'Reset your password'; $message = $this->getResetEmailTemplate($user->display_name, $magic_url); $sent = $this->email->sendEmail($email, $subject, $message); return $sent ? true : new WP_Error('email_failed', 'Failed to send reset link'); } /** * Handle magic link click */ public function handleMagicLinkClick(): void { // Check if this is a magic link request 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($_GET['email']); // Only handle magic link actions if (!in_array($action, ['magic_login', 'magic_signup', 'magic_referral', 'magic_reset'])) { return; } // Verify token $token_data = $this->verifyToken($token, $email); if (is_wp_error($token_data)) { $this->handleInvalidToken($token_data); return; } // Handle different action types 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'); } // Log the user in wp_clear_auth_cookie(); wp_set_current_user($user->ID); wp_set_auth_cookie($user->ID, true); // Trigger login action do_action('wp_login', $user->user_login, $user); // Determine redirect $redirect = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : home_url('/dash'); // Redirect wp_safe_redirect($redirect); exit; } /** * Process signup via magic link */ protected function processSignup(array $token_data): void { // Create the user account $user_id = wp_create_user( $token_data['email'], wp_generate_password(20, true, true), // Random password $token_data['email'] ); if (is_wp_error($user_id)) { wp_die('Failed to create account: ' . $user_id->get_error_message()); } // Set role $user = get_user_by('ID', $user_id); $user->set_role($token_data['role']); // Update display name if provided if (!empty($token_data['name'])) { wp_update_user([ 'ID' => $user_id, 'display_name' => $token_data['name'], 'first_name' => $token_data['name'] ]); } // Save any additional meta if (!empty($token_data['meta'])) { foreach ($token_data['meta'] as $key => $value) { update_user_meta($user_id, BASE . $key, $value); } } // Log the user in wp_set_current_user($user_id); wp_set_auth_cookie($user_id, true); // Trigger registration actions do_action('user_register', $user_id); do_action('wp_login', $user->user_login, $user); // Redirect to welcome page or dashboard wp_safe_redirect(home_url('/dash?welcome=1')); exit; } /** * Process referral signup via magic link */ protected function processReferralSignup(array $token_data): void { // Create user account $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()); } // Update user info if (!empty($token_data['name'])) { wp_update_user([ 'ID' => $user_id, 'display_name' => $token_data['name'], 'first_name' => $token_data['name'] ]); } // Store referral code in session for ReferralManager to pick up if (session_status() === PHP_SESSION_NONE) { session_start(); } $_SESSION[BASE . 'referral_code'] = $token_data['referral_code']; setcookie(BASE . 'referral_code', $token_data['referral_code'], time() + (30 * DAY_IN_SECONDS), '/'); // Process referral (this will be picked up by ReferralManager::processReferral) do_action('user_register', $user_id); // Log the user in wp_set_current_user($user_id); wp_set_auth_cookie($user_id, true); do_action('wp_login', get_user_by('ID', $user_id)->user_login, get_user_by('ID', $user_id)); // Redirect with referral welcome message wp_safe_redirect(home_url('/dash?referral_welcome=1')); exit; } /** * Process password reset */ protected function processPasswordReset(array $token_data): void { // Redirect to password reset form with token wp_safe_redirect(add_query_arg([ 'action' => 'rp', 'key' => $token_data['token'], // Could use magic token or generate WP reset key 'login' => $token_data['email'] ], wp_login_url())); exit; } /** * Generate a secure token */ protected function generateToken(string $email, string $type, array $data): string { // Create unique token $token = wp_generate_password(64, false, false); // Store token data in transient $token_data = [ 'email' => $email, 'type' => $type, 'created_at' => time(), 'expires_at' => time() + $this->token_expiry, 'data' => $data ]; $cache_key = 'magic_token_' . $token; set_transient($cache_key, $token_data, $this->token_expiry); // Also index by email for rate limiting $this->recordTokenGeneration($email); return $token; } /** * Verify a magic link token */ protected function verifyToken(string $token, string $email) { // Retrieve token data $cache_key = 'magic_token_' . $token; $token_data = get_transient($cache_key); if (!$token_data) { return new WP_Error('expired_token', 'This link has expired. Please request a new one.'); } // Verify email matches if ($token_data['email'] !== $email) { return new WP_Error('invalid_token', 'Invalid magic link'); } // Check expiration if (time() > $token_data['expires_at']) { delete_transient($cache_key); return new WP_Error('expired_token', 'This link has expired. Please request a new one.'); } // Token is valid - delete it (single use) delete_transient($cache_key); // Return merged data return array_merge($token_data['data'], [ 'email' => $token_data['email'], 'type' => $token_data['type'] ]); } /** * Rate limiting for magic link generation */ protected function checkRateLimit(string $email):bool|WP_Error { $limit_key = 'magic_link_limit_' . md5($email); $attempts = (int) get_transient($limit_key); if ($attempts >= $this->max_attempts_per_hour) { return new WP_Error( 'rate_limit_exceeded', 'Too many login attempts. Please try again in an hour.' ); } return true; } /** * Record token generation for rate limiting */ protected function recordTokenGeneration(string $email): void { $limit_key = 'magic_link_limit_' . md5($email); $attempts = (int) get_transient($limit_key); set_transient($limit_key, $attempts + 1, $this->rate_limit_window); } /** * Handle invalid/expired tokens */ protected function handleInvalidToken(WP_Error $error): void { wp_die( $error->get_error_message(), 'Invalid Link', [ 'response' => 400, 'back_link' => true ] ); } /** * Add "Send me a magic link" option to login form */ public function addMagicLinkOption(): void { ?>
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 = 'Click the button below to sign in to your account. This link expires in 15 minutes.
'; $content .= ''; $content .= 'Sign In'; $content .= '
'; $content .= 'If you didn\'t request this, you can safely ignore this email.
'; return $content; } protected function getSignupEmailTemplate(string $name, string $magic_url): string { $content = 'You\'re almost there! Click the button below to complete your registration and access your account.
'; $content .= ''; $content .= 'Complete Registration'; $content .= '
'; $content .= 'This link expires in 15 minutes.
'; return $content; } protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text): string { $content = '' . esc_html($referrer_name) . ' thinks you\'d love ' . get_bloginfo('name') . ' and invited you to join!
'; if ($reward_text) { $content .= '' . esc_html($reward_text) . '
'; $content .= ''; $content .= 'Accept Invitation'; $content .= '
'; $content .= 'This link expires in 15 minutes.
'; return $content; } protected function getResetEmailTemplate(string $name, string $magic_url): string { $content = 'Hey ' . esc_html($name) . ', we received a request to reset your password.
'; $content .= ''; $content .= 'Reset Password'; $content .= '
'; $content .= 'If you didn\'t request this, you can safely ignore this email.
'; return $content; } } new MagicLinkManager();