| | |
| | | /** |
| | | * Magic Link Authentication Manager |
| | | * |
| | | * Handles passwordless authentication via email magic links. |
| | | * Can be used for referral signups, password resets, or general login. |
| | | * NOTE: Login form integration is now handled by LoginManager.php |
| | | * This class focuses solely on magic link generation and verification |
| | | */ |
| | | class MagicLinkManager |
| | | { |
| | | protected CacheManager $cache; |
| | | protected EmailManager $email; |
| | | protected Cache $cache; |
| | | protected Cache $referral_cache; |
| | | |
| | | // Token settings |
| | | protected int $token_expiry = 900; // 15 minutes in seconds |
| | | protected int $rate_limit_window = 3600; // 1 hour |
| | | protected int $max_attempts_per_hour = 5; |
| | | |
| | | // Link types - allows different flows for different purposes |
| | | // Link types |
| | | const TYPE_LOGIN = 'login'; |
| | | const TYPE_SIGNUP = 'signup'; |
| | | const TYPE_REFERRAL = 'referral'; |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = new CacheManager('magic_links', $this->token_expiry); |
| | | $this->email = new EmailManager(); |
| | | $this->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 magic link option to login page |
| | | add_action('login_form', [$this, 'addMagicLinkOption']); |
| | | add_filter('authenticate', [$this, 'blockStandardAuth'], 30, 3); |
| | | add_action('jvb_process_login_tokens', [$this, 'processRegistrationToken'], 10, 3); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | { |
| | | // 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), |
| | | 'email' => rawurlencode($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')); |
| | | $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 |
| | | 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', |
| | |
| | | |
| | | $token = $this->generateToken($email, self::TYPE_SIGNUP, $token_data); |
| | | |
| | | // Build signup completion URL |
| | | $magic_url = add_query_arg([ |
| | | 'magic_token' => $token, |
| | | 'email' => urlencode($email), |
| | | 'email' => rawurlencode($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'); |
| | | $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 |
| | | 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'); |
| | | return new WP_Error('missing_referral', '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 |
| | | 'name' => $context['name'] ?? '', |
| | | 'role' => $context['role'] ?? 'subscriber', |
| | | 'email' => $email |
| | | ]; |
| | | |
| | | $token = $this->generateToken($email, self::TYPE_REFERRAL, $token_data); |
| | | |
| | | // Build referral signup URL |
| | | $magic_url = add_query_arg([ |
| | | 'magic_token' => $token, |
| | | 'email' => urlencode($email), |
| | | 'email' => rawurlencode($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'] ?? '' |
| | | ); |
| | | $referrer_name = $context['referrer_name'] ?? 'A friend'; |
| | | $reward_text = $context['reward_text'] ?? ''; |
| | | |
| | | $sent = $this->email->sendEmail($email, $subject, $message, $referrer_name.' invites you to see the difference at Legacy'); |
| | | $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); |
| | | |
| | | return $sent ? true : new WP_Error('email_failed', 'Failed to send referral invitation'); |
| | | $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 |
| | | 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; |
| | | return new WP_Error('user_not_found', 'No account found with this email'); |
| | | } |
| | | |
| | | $token = $this->generateToken($email, self::TYPE_RESET, [ |
| | |
| | | |
| | | $magic_url = add_query_arg([ |
| | | 'magic_token' => $token, |
| | | 'email' => urlencode($email), |
| | | 'email' => rawurlencode($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); |
| | | $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Reset Password'); |
| | | |
| | | return $sent ? true : new WP_Error('email_failed', 'Failed to send reset link'); |
| | | } |
| | |
| | | */ |
| | | 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']); |
| | | $email = sanitize_email(rawurldecode($_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)) { |
| | |
| | | return; |
| | | } |
| | | |
| | | // Handle different action types |
| | | switch ($action) { |
| | | case 'magic_login': |
| | | $this->processLogin($token_data); |
| | |
| | | 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; |
| | | } |
| | |
| | | */ |
| | | 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()); |
| | | if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) { |
| | | JVB()->error()->log('[MagicLinkManager]Could not process Signup'); |
| | | return; |
| | | } |
| | | |
| | | // 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), |
| | |
| | | wp_die('Failed to create account: ' . $user_id->get_error_message()); |
| | | } |
| | | |
| | | // Update user info |
| | | $user = get_user_by('ID', $user_id); |
| | | $user->set_role($token_data['role']); |
| | | |
| | | if (!empty($token_data['name'])) { |
| | | wp_update_user([ |
| | | 'ID' => $user_id, |
| | |
| | | ]); |
| | | } |
| | | |
| | | // Store referral code in session for ReferralManager to pick up |
| | | if (session_status() === PHP_SESSION_NONE) { |
| | | session_start(); |
| | | if (!empty($token_data['meta'])) { |
| | | foreach ($token_data['meta'] as $key => $value) { |
| | | update_user_meta($user_id, BASE . $key, $value); |
| | | } |
| | | } |
| | | $_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')); |
| | | do_action('user_register', $user_id); |
| | | do_action('wp_login', $user->user_login, $user); |
| | | |
| | | wp_safe_redirect(home_url('/dash?welcome=1')); |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * Process password reset |
| | | * 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 |
| | | { |
| | | // 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())); |
| | | $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; |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | * Handle invalid token |
| | | */ |
| | | protected function handleInvalidToken(WP_Error $error): void |
| | | { |
| | | wp_die( |
| | | $error->get_error_message(), |
| | | 'Invalid Link', |
| | | [ |
| | | 'response' => 400, |
| | | 'back_link' => true |
| | | ] |
| | | ); |
| | | wp_die($error->get_error_message()); |
| | | } |
| | | |
| | | /** |
| | | * Add "Send me a magic link" option to login form |
| | | * Handle failed login - offer magic link option |
| | | */ |
| | | public function addMagicLinkOption(): void |
| | | public function handleFailedLogin(string $username): void |
| | | { |
| | | ?> |
| | | <p class="magic-link-option"> |
| | | <a href="#" id="use-magic-link">Send me a login link instead</a> |
| | | </p> |
| | | <script> |
| | | document.getElementById('use-magic-link')?.addEventListener('click', function(e) { |
| | | e.preventDefault(); |
| | | const email = document.getElementById('user_login')?.value; |
| | | |
| | | if (!email) { |
| | | alert('Please enter your email address first'); |
| | | return; |
| | | } |
| | | |
| | | // Send magic link request |
| | | fetch('<?php echo rest_url(BASE . '/v1/magic-link/send'); ?>', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | }, |
| | | body: JSON.stringify({ |
| | | email: email, |
| | | type: 'login' |
| | | }) |
| | | }) |
| | | .then(r => r.json()) |
| | | .then(data => { |
| | | if (data.success) { |
| | | alert('Check your email! We sent you a login link.'); |
| | | } else { |
| | | alert(data.message || 'Failed to send link'); |
| | | } |
| | | }); |
| | | }); |
| | | </script> |
| | | <?php |
| | | // Could add logic here to automatically offer magic link |
| | | // after multiple failed attempts |
| | | } |
| | | |
| | | /** |
| | | * Optionally block standard password auth for certain users |
| | | * Optionally block standard password auth for magic-link-only users |
| | | */ |
| | | public function blockStandardAuth($user, $username, $password) |
| | | { |
| | | // Only block if user has magic-link-only flag |
| | | if ($user instanceof WP_User) { |
| | | $magic_only = get_user_meta($user->ID, BASE . 'magic_link_only', true); |
| | | if ($magic_only) { |
| | |
| | | |
| | | protected function getLoginEmailTemplate(string $name, string $magic_url): string |
| | | { |
| | | $content = '<h2>Hey ' . esc_html($name) . '!</h2>'; |
| | | $content .= '<p>Click the button below to sign in to your account. This link expires in 15 minutes.</p>'; |
| | | $content .= '<p style="text-align: center; margin: 30px 0;">'; |
| | | $content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Sign In</a>'; |
| | | $content .= '</p>'; |
| | | $content .= '<p style="color: #666; font-size: 14px;">If you didn\'t request this, you can safely ignore this email.</p>'; |
| | | |
| | | $content = JVB()->email()->h2('Hey ' . esc_html($name) . '!'); |
| | | $content .= '<p>Click the button below to sign in to your account instantly - no password needed!</p>'; |
| | | $content .= JVB()->email()->button($magic_url, 'Sign In Now'); |
| | | $content .= '<p>Or copy and paste this link into your browser:</p>'; |
| | | $content .= JVB()->email()->link($magic_url); |
| | | $content .= JVB()->email()->divider(); |
| | | $content .= '<p>If you didn\'t request this, you can safely ignore this email. This link expires in 15 minutes.</p>'; |
| | | return $content; |
| | | } |
| | | |
| | | protected function getSignupEmailTemplate(string $name, string $magic_url): string |
| | | { |
| | | $content = '<h2>Welcome' . ($name ? ', ' . esc_html($name) : '') . '!</h2>'; |
| | | $content .= '<p>You\'re almost there! Click the button below to complete your registration and access your account.</p>'; |
| | | $content .= '<p style="text-align: center; margin: 30px 0;">'; |
| | | $content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Complete Registration</a>'; |
| | | $content .= '</p>'; |
| | | $content .= '<p style="color: #666; font-size: 14px;">This link expires in 15 minutes.</p>'; |
| | | |
| | | $content = JVB()->email()->h2('Welcome' . ($name ? ', ' . esc_html($name) : '') . '!'); |
| | | $content .= '<p>Click the button below to complete your registration and access your account!</p>'; |
| | | $content .= JVB()->email()->button($magic_url, 'Complete Registration'); |
| | | $content .= '<p>Or copy and paste this link:</p>'; |
| | | $content .= JVB()->email()->link($magic_url); |
| | | $content .= JVB()->email()->spacer(10); |
| | | $content .= '<p><small>This link expires in 24 hours.</small></p>'; |
| | | return $content; |
| | | } |
| | | |
| | | protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text): string |
| | | protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text, array $context): string |
| | | { |
| | | $content = '<h2>Hey' . ($name ? ' ' . esc_html($name) : '') . '!</h2>'; |
| | | $content .= '<p><strong>' . esc_html($referrer_name) . '</strong> thinks you\'d love ' . get_bloginfo('name') . ' and invited you to join!</p>'; |
| | | $content = JVB()->email()->h2('Hey' . ($name ? ' ' . esc_html($name) : '') . '!'); |
| | | $content .= sprintf( |
| | | '<p><strong>%s</strong> thinks you\'d love %s!</p>', |
| | | esc_html($referrer_name), |
| | | esc_html(get_bloginfo('name')) |
| | | ); |
| | | |
| | | if ($reward_text) { |
| | | $content .= '<div style="background: #e7f5ff; padding: 20px; border-radius: 8px; margin: 20px 0;">'; |
| | | $content .= '<h3 style="margin-top: 0;">🎉 Special Offer</h3>'; |
| | | $content .= '<p>' . esc_html($reward_text) . '</p>'; |
| | | $content .= '</div>'; |
| | | if (!empty($context['message'])) { |
| | | $content .= JVB()->email()->callout(nl2br(esc_html($context['message']))); |
| | | } |
| | | |
| | | $content .= '<p style="text-align: center; margin: 30px 0;">'; |
| | | $content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Accept Invitation</a>'; |
| | | $content .= '</p>'; |
| | | $content .= '<p style="color: #666; font-size: 14px;">This link expires in 15 minutes.</p>'; |
| | | if ($reward_text) { |
| | | $content .= JVB()->email()->alert( |
| | | '<strong>Special Welcome Offer:</strong> ' . esc_html($reward_text), |
| | | 'success' |
| | | ); |
| | | } |
| | | |
| | | return $content; |
| | | } |
| | | |
| | | protected function getResetEmailTemplate(string $name, string $magic_url): string |
| | | { |
| | | $content = '<h2>Reset Your Password</h2>'; |
| | | $content .= '<p>Hey ' . esc_html($name) . ', we received a request to reset your password.</p>'; |
| | | $content .= '<p style="text-align: center; margin: 30px 0;">'; |
| | | $content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Reset Password</a>'; |
| | | $content .= '</p>'; |
| | | $content .= '<p style="color: #666; font-size: 14px;">If you didn\'t request this, you can safely ignore this email.</p>'; |
| | | $content .= JVB()->email()->button($magic_url, 'Join Now'); |
| | | $content .= '<p>Or copy and paste this link:</p>'; |
| | | $content .= JVB()->email()->link($magic_url); |
| | | $content .= JVB()->email()->spacer(10); |
| | | $content .= '<p><small>This invitation expires in 14 days.</small></p>'; |
| | | |
| | | return $content; |
| | | } |
| | | } |
| | | |
| | | new MagicLinkManager(); |