prefix . self::TABLE; if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) { self::create_table(); } } public static function create_table() { global $wpdb; $table = $wpdb->prefix . self::TABLE; $charset = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$table} ( id bigint(20) unsigned NOT NULL auto_increment, user_id bigint(20) unsigned NOT NULL, token varchar(64) NOT NULL, email varchar(100) NOT NULL, created_at datetime NOT NULL default CURRENT_TIMESTAMP, expires_at datetime NOT NULL, used_at datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY token (token), KEY user_id (user_id), KEY expires_at (expires_at) ) {$charset};"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } public static function register_rest_routes() { register_rest_route('beep/v1', '/magic-link', [ 'methods' => 'POST', 'callback' => [__CLASS__, 'request_magic_link'], 'permission_callback' => '__return_true', ]); } public static function request_magic_link($request) { $email = sanitize_email($request->get_param('email')); if (!$email || !is_email($email)) { return new WP_Error('invalid_email', 'Please enter a valid email address.', ['status' => 400]); } $user = get_user_by('email', $email); if (!$user) { // Don't reveal whether an email exists return ['success' => true, 'message' => 'If that email is registered, a login link has been sent.']; } // Check rate limit (one request per 5 minutes per email) $rate_key = 'beep_ml_' . md5($email); $last_request = get_transient($rate_key); if ($last_request) { return new WP_Error( 'rate_limited', 'Please wait a few minutes before requesting another login link.', ['status' => 429] ); } // Generate token $token = wp_generate_password(self::TOKEN_LENGTH, false); $hashed_token = hash('sha256', $token); $expires_at = date('Y-m-d H:i:s', time() + (self::EXPIRY_HOURS * 3600)); // Store in database global $wpdb; $table = $wpdb->prefix . self::TABLE; $wpdb->insert($table, [ 'user_id' => $user->ID, 'token' => $hashed_token, 'email' => $email, 'expires_at' => $expires_at, ]); // Set rate limit set_transient($rate_key, true, 5 * 60); // Build magic link $login_url = add_query_arg([ 'beep_magic' => $token, 'uid' => $user->ID, ], home_url('/')); // Email content $site_name = get_bloginfo('name'); $subject = sprintf('[%s] Your login link', $site_name); $message = sprintf( "Hi %s,\n\nClick the link below to sign in to %s:\n\n%s\n\nThis link expires in %d hours.\n\nIf you didn't request this, you can ignore this email.\n\n— The %s team", $user->display_name, $site_name, $login_url, self::EXPIRY_HOURS, $site_name ); // Send email $sent = wp_mail($email, $subject, $message); if (!$sent) { return new WP_Error('email_failed', 'Failed to send email. Please try again.', ['status' => 500]); } return ['success' => true, 'message' => 'Check your email for the login link!']; } public static function handle_magic_link_login() { if (!isset($_GET['beep_magic']) || !isset($_GET['uid'])) { return; } $token = sanitize_text_field($_GET['beep_magic']); $user_id = (int) $_GET['uid']; if (!$token || !$user_id) { return; } // Verify token global $wpdb; $table = $wpdb->prefix . self::TABLE; $hashed_token = hash('sha256', $token); $record = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE token = %s AND user_id = %d AND used_at IS NULL AND expires_at > %s", $hashed_token, $user_id, current_time('mysql') )); if (!$record) { wp_die('This login link is invalid or has expired. Please request a new one.'); } // Mark as used $wpdb->update( $table, ['used_at' => current_time('mysql')], ['id' => $record->id] ); // Log the user in $user = get_user_by('id', $user_id); if ($user) { wp_set_current_user($user_id); wp_set_auth_cookie($user_id); do_action('wp_login', $user->user_login, $user); // Redirect to home or beep page $redirect_url = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : home_url('/'); wp_redirect($redirect_url); exit; } } public static function cleanup_expired_tokens() { // Run once per session static $run = false; if ($run) { return; } $run = true; global $wpdb; $table = $wpdb->prefix . self::TABLE; $wpdb->query($wpdb->prepare( "DELETE FROM {$table} WHERE expires_at < %s OR (used_at IS NOT NULL AND DATE_ADD(used_at, INTERVAL 1 HOUR) < %s)", current_time('mysql'), current_time('mysql') )); } public static function render_login_form() { ob_start(); ?>