From 42fa8304ddb811b0f725f245130f70c0f5e86a6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 04 Nov 2025 06:12:02 +0000
Subject: [PATCH] =Refactored LoginManager to be more extensible and configurable, as well as an AjaxRateLimiter
---
inc/managers/LoginManager.php | 2220 +++++++++++++++++++++++++++++++++--------------------------
1 files changed, 1,237 insertions(+), 983 deletions(-)
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 7bce0db..dd12a81 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -1,1054 +1,1308 @@
<?php
namespace JVBase\managers;
+use JVBase\blocks\CustomBlocks;
+use JVBase\forms\TaxonomySelector;
use JVBase\meta\MetaManager;
+use JVBase\meta\MetaForm;
+use JVBase\managers\AjaxRateLimiter;
+use JVBase\utility\Features;
use WP_Error;
use WP_User;
if (!defined('ABSPATH')) {
- exit; // Exit if accessed directly
+ exit;
}
class LoginManager
{
- private array|null $invitation_data = null;
- protected array $inviteData = [];
- private array $allowed_file_types = [
- 'image/jpeg',
- 'image/png',
- 'image/gif',
- 'application/pdf'
- ];
- private int $max_file_size = 5242880; // 5MB in bytes
+ protected Features $siteFeatures;
+ protected ?MagicLinkManager $magicLink = null;
+ protected ?MetaForm $metaForm = null;
+ protected EmailManager $emailManager;
+ protected AjaxRateLimiter $rateLimiter;
- public function __construct()
- {
- // Common login page customization
- add_action('login_enqueue_scripts', array($this, 'loginStyles'));
- add_action('login_header', array($this, 'loginHeader'), 0);
- add_action('login_footer', array($this, 'loginFooter'));
- // Login page filters
- add_filter('login_headerurl', array($this, 'logoUrl'));
- add_filter('login_headertext', array($this, 'logoTitle'));
- add_filter('login_message', array($this, 'loginMessage'));
- add_filter('login_errors', array($this, 'loginErrors'));
+ protected array $forms =[];
+ protected array $labels = [];
+ protected array $fields = [];
+ protected ?string $action = null;
+ protected string $title = '';
- // Login success handling
- add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
+ // Token handlers registry
+ protected array $tokenHandlers = [];
+ protected array $messageHandlers = [];
- // Registration-specific hooks
- if ($this->isRegistrationPage()) {
- $this->initRegistrationHooks();
- }
- }
+ private array $allowed_file_types = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif',
+ 'application/pdf'
+ ];
+ private int $max_file_size = 5242880; // 5MB in bytes
- /**
- * Check if we're on the registration page
- */
- private function isRegistrationPage(): bool
- {
- return isset($_GET['action']) && $_GET['action'] === 'register';
- }
+ public function __construct()
+ {
+ $this->siteFeatures = Features::forSite();
+ $this->metaForm = new MetaForm();
+ $this->emailManager = new EmailManager();
+ $this->rateLimiter = new AjaxRateLimiter();
- /**
- * Initialize registration-specific hooks
- */
- private function initRegistrationHooks(): void
- {
- add_action('register_form', array($this, 'addRegistrationFields'));
- add_action('login_header', array($this, 'addRegistrationScript'));
- add_filter('registration_errors', array($this, 'registrationErrorsFilter'), 10, 3);
- add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
- add_action('login_head', array($this, 'modifyRegistrationForm'));
- add_action('register_form', array($this, 'addUploadSupport'));
- add_filter('pre_user_login', array($this, 'setUserLogin'), 1);
- add_filter('pre_user_email', array($this, 'setUserEmail'), 1);
- add_filter('register_message', array($this, 'customRegisterMessage'));
- add_filter('wp_login_errors', array($this, 'registrationSuccessMessage'), 10, 2);
- add_filter('login_form_top', array($this, 'loginFormTop'));
- add_filter('login_form_bottom', array($this, 'loginFormBottom'));
- add_filter('login_form_middle', array($this, 'loginFormMiddle'));
+ // Register default token handlers
+ $this->registerDefaultHandlers();
- // Remove default username requirement for registration
- remove_filter('registration_errors', 'registration_auth_pass_filter', 10);
- }
+ // Initialize magic link support if enabled
+ if ($this->siteFeatures->has('magicLink')) {
+ $this->initMagicLinkSupport();
+ }
- /**
- * Combined login styles for both login and registration
- */
- public function loginStyles(): void
- {
- do_action('jvbLoginStyles');
- }
+ // Create login page if it doesn't exist
+ $this->ensureLoginPageExists();
- /**
- * Login header - used for both login and registration
- */
- public function loginHeader(): void
- {
- ?>
- <script type="text/javascript">
- document.addEventListener('DOMContentLoaded', function() {
- let loginLabel = document.querySelector('label[for="user_login"');
- loginLabel.innerHTML = '<?= jvbIcon('email', ['size' => 20]); ?> Your Email';
- let passwordLabel = document.querySelector('label[for="user_pass"');
- passwordLabel.innerHTML = '<?= jvbIcon('password', ['size' => 20]); ?> Your Password';
+ // Redirect wp-login.php to custom page
+ add_action('login_init', [$this, 'redirectToCustomLogin']);
+ add_action('template_include', [$this, 'renderLoginPage']);
- document.querySelector('form').classList.add('loaded');
- });
+ add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15);
- </script>
- <?php
- }
+ // Handle form submissions via AJAX
+ add_action('wp_ajax_nopriv_jvb_login', [$this, 'handleAjaxLogin']);
+ add_action('wp_ajax_nopriv_jvb_register', [$this, 'handleAjaxRegister']);
+ add_action('wp_ajax_nopriv_jvb_lostpassword', [$this, 'handleAjaxLostPassword']);
+ add_action('wp_ajax_nopriv_jvb_resetpass', [$this, 'handleAjaxResetPassword']);
- /**
- * Login footer with donate section
- */
- public function loginFooter(): void
- {
- do_action('jvbLoginFooter');
+ // Login success handling
+ add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
- }
+ // Allow other features to register handlers
+ do_action('jvbLoginManagerInit', $this);
+ }
- /**
- * Logo URL
- */
- public function logoUrl(): string
- {
- return home_url();
- }
+ /**************************************************************************
+ * SETUP & CONFIGURATION
+ **************************************************************************/
- /**
- * Logo title
- */
- public function logoTitle(): string
- {
- return get_bloginfo('name');
- }
+ /**
+ * Redirect wp-login.php to custom login page
+ */
+ public function redirectToCustomLogin(): void
+ {
+ // Don't redirect if AJAX or REST
+ if ((defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) {
+ return;
+ }
+ // Build custom login URL with all query args
+ $custom_login_page = home_url('/login');
+ $query_args = $_GET;
- /**
- * Login message - handles both login and registration
- */
- public function loginMessage(string $message): string
- {
- if ($this->isRegistrationPage()) {
- if (jvbSiteHasInvitations() && $this->fromInvite()) {
- $data = JVB()->routes('invites')->verifyInvitation(sanitize_text_field($_GET['invite']), sanitize_email($_GET['email']));
- $name = $data->name;
- $inviters = json_decode($data->inviters, true);
- $names = [];
- foreach ($inviters as $inviter) {
- $artist = jvbContentFromUser((int)$inviter['user_id']);
- $names[] = ($artist['name'] === '') ? $artist['display_name'] : $artist['name'];
- }
- $message = (count($names) > 1) ? 'are already here, and have invited you to join in!' : ' is already here, and invited you to join in!';
- return '<h2>Join the Scene, '.$name.'</h2>
- <p style="text-align:center;">'.jvbCommaList($names).$message.'</p>';
- }
- if (jvbSiteHasFavourites() && $this->fromFavourites()) {
- return '<h2>'.JVB_LOGIN['login_from_favourite_header']??'Save your Favourites'.'</h2>';
- }
- return '<h2>'.JVB_LOGIN['join_header'].'</h2>';
- } else {
- if (jvbSiteHasFavourites()) {
- $login = (!$this->fromFavourites()) ? '<h2>'.JVB_LOGIN['login_header'].'</h2>' : '<h2>'.JVB_LOGIN['login_from_favourite_header'].'</h2>';
- } else {
- $login = '<h2>'.JVB_LOGIN['login_header'].'</h2>';
+ // Remove WordPress internal args
+ unset($query_args['interim-login'], $query_args['wp-auth-check']);
+
+ if (!empty($query_args)) {
+ $custom_login_page = add_query_arg($query_args, $custom_login_page);
+ }
+
+ wp_safe_redirect($custom_login_page);
+ exit;
+ }
+ protected function getRegistrationFormFields():array
+ {
+ $form = get_option(BASE.'registration_form_fields');
+ if (!$form) {
+ $form = [];
+
+ $select = [];
+ //Basic fields, for any
+ $fields = [
+ 'name' => [
+ 'type' => 'text',
+ 'required' => true,
+ 'label' => 'Your Name',
+ 'placeholder'=> 'Mister Meseeks'
+ ],
+ 'email' => [
+ 'type' => 'email',
+ 'required' => true,
+ 'label' => 'Your Email',
+ 'placeholder'=> 'look@me.com'
+ ]
+ ];
+ if (count(JVB_USER) > 1) {
+ foreach (JVB_USER as $slug => $config) {
+ if (!array_key_exists('can_register', $config) || !$config['can_register']) {
+ continue;
+ }
+ $icon = $config['icon'] ?? '';
+ $icon = ($icon !== '') ? jvbIcon($icon) : '';
+ $select[$slug] = '<span class="label">'.$icon.$config['label'].'</span><span class="text">'.$config['register']['text']??''.'</span>';
+ if (!empty($config['register']['fields']??[])){
+ foreach ($config['register']['fields'] as $field) {
+ $field['condition'] = [
+ 'field' => 'user_select',
+ 'value' => $slug,
+ 'operator' => '=='
+ ];
+ $fields[] = $field;
+ }
+ }
+ }
+ if (!empty($select)) {
+ $select = array_merge(
+ [
+ 'subscriber' => 'Subscriber',
+ ],
+ $select
+ );
+ $form = array_merge(
+ [
+ 'user_select' => [
+ 'type' => 'radio',
+ 'label' => 'Register as',
+ 'options' => $select,
+ 'required' => true,
+ 'default' => 'subscriber'
+ ]
+ ],
+ $fields
+ );
+ }
+ }else {
+ $form = $fields;
+ }
+ update_option(BASE.'registration_form_fields', $form);
+ }
+ return $form;
+
+ }
+
+ protected function setupFields():void
+ {
+ $fields = [];
+ switch($this->action) {
+ case 'register':
+ $fields = $this->getRegistrationFormFields();
+ break;
+ case 'lostpassword':
+ $fields = [
+ 'user_email' => [
+ 'type' => 'email',
+ 'label' => __('Email Address', 'jvb'),
+ 'required' => true,
+ 'placeholder' => 'look@me.com',
+ ],
+ ];
+ break;
+ case 'rp':
+ case 'resetpass':
+ $fields = [
+ 'pass1' => [
+ 'type' => 'text',
+ 'subtype' => 'password',
+ 'label' => __('New Password', 'jvb'),
+ 'required' => true,
+ ],
+ 'pass2' => [
+ 'type' => 'text',
+ 'subtype' => 'password',
+ 'label' => __('Confirm Password', 'jvb'),
+ 'required' => true,
+ ],
+ ];
+ break;
+ case 'login':
+ $fields = [
+ 'user_email' => [
+ 'type' => 'email',
+ 'label' => __('Email Address', 'jvb'),
+ 'required' => true,
+ 'placeholder' => 'look@me.com',
+ ],
+ 'user_password' => [
+ 'type' => 'text',
+ 'subtype'=> 'password',
+ 'label' => __('Password', 'jvb'),
+ 'required' => true,
+ ],
+ 'remember_me' => [
+ 'type' => 'true_false',
+ 'label' => __('Remember Me', 'jvb'),
+ 'default' => true
+ ]
+ ];
+ break;
+ case 'postpass':
+ $fields = [
+ 'post_password' => [
+ 'type' => 'text',
+ 'subtype' => 'password',
+ 'label' => __('Password', 'jvb'),
+ 'required' => true,
+ 'hint' => 'This post is password protected. Please enter the password to view it.',
+ ],
+ ];
+ break;
+ case 'confirmaction':
+
+ break;
+
+ }
+ $this->fields = $fields;
+ }
+
+ /**
+ * Ensure login page exists
+ */
+ protected function ensureLoginPageExists(): void
+ {
+ $login_page = $this->getLoginPage();
+
+ if (!$login_page || !is_int($login_page)) {
+ $page_id = get_page_by_path('login');
+ if (!$page_id) {
+ $page_id = wp_insert_post([
+ 'post_title' => 'Login',
+ 'post_name' => 'login',
+ 'post_content' => '[jvb_login_form]',
+ 'post_status' => 'publish',
+ 'post_type' => 'page',
+ 'post_author' => 1
+ ]);
}
- return (empty($message)) ? $login : $login.$message;
- }
- }
-
- protected function fromFavourites():bool
+ if ($page_id && !is_wp_error($page_id)) {
+ if (is_object($page_id)) {
+ $page_id = (int)$page_id->ID;
+ }
+ update_option(BASE.'login_page', $page_id);
+ // Hide from menus/search
+ update_post_meta($page_id, '_wp_page_template', 'default');
+ update_post_meta($page_id, BASE . 'exclude_from_search', true);
+ }
+ }
+ }
+ public function getLoginPage():int|false
{
- return array_key_exists('type', $_GET) && $_GET['type'] === 'favourites';
+ return (int)get_option(BASE.'login_page');
}
- /**
- * Customize login error messages
- */
- public function loginErrors(string $error): string
- {
- return str_replace(
- array(
- 'The password you entered for the username',
- 'Invalid username',
- 'Unknown username',
- 'Unknown email address'
- ),
- array(
- 'Wrong password',
- 'We can\'t find that username',
- 'We can\'t find that username',
- 'We can\'t find that email'
- ),
- $error
- );
- }
+ public function isLoginPage():bool
+ {
+ return is_page($this->getLoginPage());
+ }
- /**
- * Handle successful login
- */
- public function handleSuccessfulLogin(string $username, WP_User $user): void
- {
- if (isOurPeople() && !user_can($user, 'manage_options')) {
- wp_redirect(get_home_url(null, '/dash'));
- exit;
- }
- }
+ public static function isLogin():bool
+ {
+ $self = new self;
+ return $self->isLoginPage();
+ }
- // ===== REGISTRATION-SPECIFIC METHODS =====
-
- /**
- * Set user login for registration
- */
- public function setUserLogin(string $login): string
- {
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
- if (!empty($user_type)) {
- $email_field = $user_type . '_email';
- if (isset($_POST[$email_field])) {
- $email = sanitize_email($_POST[$email_field]);
- if (is_email($email)) {
- return $email;
- }
- }
- }
- return $login;
- }
-
- /**
- * Set user email for registration
- */
- public function setUserEmail(string $email): string
- {
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
- if (!empty($user_type)) {
- $email_field = $user_type . '_email';
- if (isset($_POST[$email_field])) {
- $email = sanitize_email($_POST[$email_field]);
- if (is_email($email)) {
- return $email;
- }
- }
- }
- return $email;
- }
-
- /**
- * Modify registration form
- */
- public function modifyRegistrationForm(): void
- {
- if (!$this->isRegistrationPage()) {
- return;
- }
-
- ?>
- <script type="text/javascript">
- document.addEventListener('DOMContentLoaded', function() {
- // Hide default fields
- const defaultFields = document.getElementById('registerform').querySelectorAll('p');
- defaultFields.forEach(field => {
- if (field.querySelector('label[for="user_login"]') ||
- field.querySelector('label[for="user_email"]')) {
- field.remove();
- }
- });
-
- // Hide the default registration info text
- const regInfo = document.querySelector('.message.register');
- if (regInfo) {
- regInfo.style.display = 'none';
- }
-
- <?php
- if ($this->fromInvite()) {
- $this->handleArtistInvitation();
- }
- ?>
-
- // Move submit button to the end of the form
- const submitButton = document.getElementById('registerform').querySelector('.submit');
- if (submitButton) {
- document.getElementById('registerform').appendChild(submitButton);
- }
- });
- </script>
- <?php
- }
-
- /**
- * Handle artist invitation pre-fill
- */
- protected function handleArtistInvitation(): void
- {
- $token = sanitize_text_field($_GET['invite']);
- $email = sanitize_email($_GET['email']);
- $data = JVB()->routes('invites')->verifyInvitation($token, $email);
-
- ?>
- document.querySelector('input#artist').checked = true;
- document.querySelector('#artist_first_name').value = '<?=$data->name?>';
- document.querySelector('#artist_email').value = '<?=$email?>';
- <?php
- if ($data->to_shop) {
- ?>
- document.querySelector('#artist_shop').value = '<?=$data->shop?>';
- <?php
- }
- ?>
- let form = document.getElementById('registerform')
- let input = document.createElement('input');
- let email = input.cloneNode(true);
- input.type = 'hidden';
- input.name = 'invite_token';
- input.value = '<?= $token ?>';
- email.type = 'hidden';
- email.name = 'invite_email';
- email.value = '<?= $email?>';
- form.append(input);
- form.append(email);
- <?php
- }
-
- /**
- * Add upload support for registration
- */
- public function addUploadSupport(): void
- {
- ?>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- const form = document.getElementById('registerform');
- if (form) {
- form.enctype = 'multipart/form-data';
- }
- });
- </script>
- <?php
- }
-
- /**
- * Add registration script
- */
- public function addRegistrationScript(): void
- {
- if (!$this->isRegistrationPage()) {
- return;
- }
- ?>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
-
- // Initialize user type selection
- function initUserTypeSelection() {
- const userTypeRadios = document.querySelectorAll('input[name="user_type"]');
- const fieldGroups = document.querySelectorAll('.field-group');
-
- userTypeRadios.forEach(radio => {
- radio.addEventListener('change', function() {
- fieldGroups.forEach(group => group.classList.remove('active'));
- const selectedType = this.value;
- const targetGroup = document.querySelector(`.field-group[data-type="${selectedType}"]`);
- if (targetGroup) {
- targetGroup.classList.add('active');
- }
- });
- });
-
- const checkedRadio = document.querySelector('input[name="user_type"]:checked');
- if (checkedRadio) {
- const targetGroup = document.querySelector(`.field-group[data-type="${checkedRadio.value}"]`);
- if (targetGroup) {
- targetGroup.classList.add('active');
- }
- }
- }
-
- // Initialize shop selection
- function initShopSelection() {
- let form = document.getElementById('registerform');
- form.addEventListener('change', (e) => {
- if(e.target.id === 'artist_shop' || e.target.id === 'artist_city'){
- let next = e.target.parentNode.nextElementSibling;
- let input = next.querySelector('input');
-
- if(e.target.value === 'other'){
- next.style.display = 'block';
- next.style.animation = 'fadeIn 0.3s ease';
- input.required = true;
- input.focus();
- }else{
- input.required = false;
- input.value = '';
- }
- }
- });
- }
-
- // Initialize file upload handling
- function initFileUpload() {
- const fileInput = document.getElementById('certification_file');
- const filePreview = document.querySelector('.file-preview');
- const filePreviewName = document.querySelector('.file-preview-name');
- const fileError = document.querySelector('.file-error');
- const removeButton = document.querySelector('.file-preview-remove');
-
- if (!fileInput || !filePreview || !filePreviewName || !fileError || !removeButton) {
- return;
- }
-
- const maxSize = parseInt(fileInput.dataset.maxSize || 5242880);
-
- fileInput.addEventListener('change', function(e) {
- const file = e.target.files[0];
- fileError.classList.remove('active');
-
- if (file) {
- const validTypes = ['.jpg','.jpeg','.png','.gif','.pdf'];
- const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
-
- if (!validTypes.includes(fileExtension)) {
- showError('Please upload a valid file type (JPG, PNG, GIF, or PDF)');
- fileInput.value = '';
- return;
- }
-
- if (file.size > maxSize) {
- showError('File size must be less than 5MB');
- fileInput.value = '';
- return;
- }
-
- filePreviewName.textContent = file.name;
- filePreview.classList.add('active');
- } else {
- filePreview.classList.remove('active');
- }
- });
-
- removeButton.addEventListener('click', function() {
- fileInput.value = '';
- filePreview.classList.remove('active');
- fileError.classList.remove('active');
- });
-
- function showError(message) {
- fileError.textContent = message;
- fileError.classList.add('active');
- filePreview.classList.remove('active');
- }
- }
-
- // Initialize all components
- initUserTypeSelection();
- initShopSelection();
- initFileUpload();
- });
- </script>
- <?php
- }
-
- /**
- * Add registration fields
- */
- public function addRegistrationFields(): void
- {
- echo '<input type="hidden" name="user_pass" value="' . wp_generate_password() . '">';
- ?>
- <div class="registration-intro">
- <?php
- foreach (JVB_LOGIN['join_intro']??[] as $intro) {
- echo '<p>'.$intro.'</p>';
- }
- ?>
-
- <?php if ($this->fromFavourites()): ?>
- <div class="favourites-login-message">
- <ul class="benefits-list">
- <?php
- foreach (JVB_LOGIN['from_favourites_benefits']??[] as $benefit) {
- echo '<li>'.$benefit.'</li>';
- }
- ?>
- </ul>
- </div>
- <?php endif; ?>
- </div>
-
- <?php
- if (array_key_exists('choose', JVB_LOGIN)) {
- ?>
- <h3><?= JVB_LOGIN['choose']?></h3>
- <?php
+ /**************************************************************************
+ TOKEN & MESSAGE HANDLERS
+ Extensible by other classes
+ **************************************************************************/
+ public function registerTokenHandler(string $token_key, callable $handler, int $priority = 10): void
+ {
+ if (!isset($this->tokenHandlers[$priority])) {
+ $this->tokenHandlers[$priority] = [];
}
- ?>
- <?php
- if (count(JVB_USER) > 1) {
- $this->renderUserTypeSelection();
+ $this->tokenHandlers[$priority][$token_key] = $handler;
+ ksort($this->tokenHandlers);
+ }
+
+ public function registerMessageHandler(string $type, callable $handler, ?callable $condition = null): void
+ {
+ $this->messageHandlers[$type] = [
+ 'handler' => $handler,
+ 'condition' => $condition
+ ];
+ }
+
+ protected function registerDefaultHandlers(): void
+ {
+ // Invitation handler
+ if ($this->siteFeatures->has('invitations')) {
+ $this->registerTokenHandler('invite', function($token, $email, $user_id) {
+ if (isset($_POST['invite_token'])) {
+ JVB()->routes('invites')->acceptInvitation(
+ sanitize_text_field($_POST['invite_token']),
+ sanitize_email($_POST['invite_email']),
+ $user_id
+ );
+ }
+ });
+
+ $this->registerMessageHandler('invitation',
+ function() {
+ $data = JVB()->routes('invites')->verifyInvitation(
+ sanitize_text_field($_GET['invite']),
+ sanitize_email($_GET['email'])
+ );
+ $name = $data->name;
+ $inviters = json_decode($data->inviters, true);
+ $names = [];
+
+ foreach ($inviters as $inviter) {
+ $artist = jvbContentFromUser((int)$inviter['user_id']);
+ $names[] = ($artist['name'] === '') ? $artist['display_name'] : $artist['name'];
+ }
+
+ $message = (count($names) > 1)
+ ? 'are already here, and have invited you to join in!'
+ : ' is already here, and invited you to join in!';
+
+ return '<h2>Join the Scene, '.$name.'</h2>
+ <p style="text-align:center;">'.jvbCommaList($names).$message.'</p>';
+ },
+ function() {
+ return isset($_GET['invite']) && isset($_GET['email']);
+ }
+ );
+ }
+
+ // List sharing handler (Favourites)
+ if ($this->siteFeatures->has('favourites')) {
+ $this->registerTokenHandler('list_token', function($token, $email, $user_id) {
+ if (!empty($_GET['list_token']) && !empty($_GET['email'])) {
+ JVB()->routes('favourites')->acceptListInvitation(
+ sanitize_text_field($_GET['list_token']),
+ sanitize_email($_GET['email']),
+ $user_id
+ );
+ }
+ });
+
+ $this->registerMessageHandler('favourites',
+ function() {
+ return '<h2>'.(JVB_LOGIN['login_from_favourite_header'] ?? 'Save your Favourites').'</h2>';
+ },
+ function() {
+ return isset($_GET['type']) && $_GET['type'] === 'favourites';
+ }
+ );
+ }
+
+ // Referral handler - FIXED VERSION
+ $this->registerTokenHandler('referral_code', function($code, $email, $user_id) {
+ // $code is already sanitized from processTokenHandlers
+ if (session_status() === PHP_SESSION_NONE) {
+ session_start();
+ }
+ $_SESSION[BASE . 'referral_code'] = $code;
+ setcookie(
+ BASE . 'referral_code',
+ $code,
+ time() + (86400 * 30),
+ '/'
+ );
+ }, 5);
+ }
+
+ protected function initMagicLinkSupport(): void
+ {
+ if (!Features::forSite()->has('magicLink')) {
+ return;
+ }
+ $this->magicLink = new MagicLinkManager();
+ }
+
+
+
+ /*********************************************************************
+ RENDERING
+ *********************************************************************/
+ public function renderLoginPage(string $template):string
+ {
+ if (!$this->isLoginPage()) {
+ return $template;
+ }
+ $this->setup();
+ ob_start();
+ jvbInlineStyles('nav');
+ jvbInlineStyles('dash');
+ jvbInlineStyles('forms');
+ $this->customStyles();
+
+ $this->renderHeader();
+ $this->renderForms();
+ $this->renderFooter();
+
+ echo ob_get_clean();
+ return '';
+ }
+
+ protected function setup():void
+ {
+ if (array_key_exists('action', $_GET)) {
+ switch ($_GET['action']){
+ case 'lostpassword':
+ case 'retrievepassword': // Alias
+ $action = 'lostpassword';
+ break;
+ case 'rp':
+ case 'resetpass':
+ $action = 'resetpass';
+ break;
+ default:
+ $action = $_GET['action'];
+ }
} else {
- ?>
- <p>
- <label for="first_name" class="required-field">First Name</label>
- <input type="text" id="first_name" name="first_name" class="input">
- </p>
- <p>
- <label for="email" class="required-field">Email</label>
- <input type="email" id="email" name="email" class="input">
- </p>
- <?php
+ $action = 'login';
}
- if ($this->invitation_data) {
- ?>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- const artistRadio = document.getElementById('artist');
- if (artistRadio) {
- artistRadio.checked = true;
- artistRadio.dispatchEvent(new Event('change'));
- }
- const emailField = document.getElementById('artist_email');
- if (emailField) {
- emailField.value = '<?= esc_js($this->invitation_data['email']); ?>';
- emailField.readOnly = true;
- }
+ $this->action = $action;
+ $this->setupLabels();
+ $this->setupFields();
+ $this->setupTitle();
+ }
- const shopSelect = document.getElementById('artist_shop');
- if (shopSelect) {
- shopSelect.value = '<?= esc_js($this->invitation_data['shop_id']); ?>';
- shopSelect.readOnly = true;
- }
- });
- </script>
- <input type="hidden" name="invitation_token" value="<?= sanitize_text_field($_GET['invite']) ?>">
- <input type="hidden" name="invitation_email" value="<?= sanitize_email($_GET['email']) ?>">
- <?php
- }
- }
+ protected function setupTitle():void
+ {
+ switch ($this->action) {
+ case 'lostpassword':
+ $title = 'Lost Your Password?';
+ break;
+ case 'resetpass':
+ $title = 'Reset Your Password';
+ break;
+ case 'register':
+ $title = 'Create Your Account';
+ break;
+ default:
+ $title = 'Log In To Your Account';
+ }
+ $this->title = $title;
+ }
- protected function renderUserTypeSelection():void
+ protected function customStyles():void
+ {
+ $logo = get_theme_mod('custom_logo');
+ $small = $large = '';
+ if ($logo) {
+ $small = wp_get_attachment_image_src($logo, 'medium')[0];
+ $large = wp_get_attachment_image_src($logo, 'large')[0];
+
+ }
+ echo '<style>
+ .login header,
+ .login footer {
+ display: none;
+ }
+ .login main {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ justify-content: center;
+ position: relative;
+ }
+ .login main::before {
+ background-size: 20vw;
+ inset: 0;
+ z-index: 0;
+ content: "";
+ background-image: url("'.$small.'");
+ background-repeat: no-repeat;
+ position: absolute;
+ background-position: 40vw 1rem;
+ }
+ .login main .login-box {
+ --gap: .75rem;
+ padding: 1rem;
+ border-radius: var(--outerRadius);
+ background-color: var(--overlay-heavy);
+ box-shadow: var(--shadow-right), var(--shadow-down);
+ margin: 15vh auto 0!important;
+ }
+ .login main .login-box,
+ .login main .navigation {
+ z-index: 5;
+ max-width: 90vw!important;
+ }
+ .login main .navigation {
+ padding: 0 1rem;
+ margin: 0 auto!important;
+ font-size: var(--small);
+ }
+ .login-box .button {
+ --height: 2.5rem;
+ width: 100%;
+ }
+ .login-box .options {
+ padding: 0 .5rem;
+ }
+ label[for="user_select-subscriber"] {
+ position: absolute;
+ left: var(--offScreen);
+ }
+
+ @media (min-width:768px) {
+ .login main .navigation,
+ .login main .login-box {
+ max-width: 60vw!important;
+ margin: 0 2rem 0 auto!important;
+ }
+ .login main .login-box {
+ padding: 2rem;
+ --gap: 2rem;
+ }
+ .login main .navigation {
+ padding: 0 var(--offHeight);
+ }
+
+ .login-box .options {
+ padding: 0 4rem;
+ }
+ .login main::before {
+ background-size: 80vw;
+ inset: -5vw;
+ background-image: url("'.$large.'");
+ opacity: .25;
+ transform: rotate(-5deg);
+ background-position: -10vw center;
+ }
+ }
+ </style>';
+ }
+
+ protected function renderForms():void
{
+ $form = $this->action.'form';
- // Get list of tattoo shops and cities
- $shops = get_terms(array(
- 'taxonomy' => 'jvb_shop',
- 'hide_empty' => true
- ));
-
- $cities = get_terms(array(
- 'taxonomy' => 'jvb_city',
- 'hide_empty' => false,
- ));
?>
- <div class="user-type-section">
+ <section class="login-box col btw">
+ <h1><?=$this->labels['title']?></h1>
+ <?= $this->labels['description'] ?>
+ <form name="<?=$form?>" method="post" data-action="jvb_<?=$this->action?>">
+ <?php wp_nonce_field('jvb_'.$this->action, '_wpnonce'); ?>
+ <input type="hidden" name="action" value="jvb_<?=$this->action?>">
+ <input type="hidden" name="redirect_to" value="<?= esc_attr($_GET['redirect_to'] ?? '') ?>">
+ <input type="hidden" name="request_id" value="<?= wp_generate_password(16, false) ?>">
+ <?php
+ $this->addHiddenTokenFields();
+
+ foreach ($this->fields as $name => $config) {
+ $this->metaForm->render($name, '', $config);
+ }
+
+ $this->maybeTurnstile();
+ ?>
+ <div class="row btw nowrap">
+ <button type="submit" class="button button-primary button-large">Log In</button>
+ <?php $this->maybeMagicLink(); ?>
+ </div>
+ </form>
+
+ <?php
+ if (is_array($this->labels['extra'])) {
+ echo '<div class="extra">';
+ foreach($this->labels['extra'] as $extra) {
+ echo '<p>'.$extra.'</p>';
+ }
+ echo '</div>';
+ } else if ($this->labels['extra']!=='') {
+ echo '<div class="extra">'.$this->labels['extra'].'</div>';
+ }
+ ?>
+
+ <div class="options row btw">
+ <?php
+ switch ($this->action) {
+ case 'login': ?>
+ <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
+ <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
+ <?php
+ break;
+ case 'register': ?>
+ <a href="<?= get_the_permalink() ?>">Or Login</a>
+ <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
+ <?php
+ break;
+ case 'lostpassword': ?>
+ <a href="<?= get_the_permalink() ?>">Login Instead</a>
+ <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
+ <?php
+ break;
+
+ }
+ ?>
+
+ </div>
+ </section>
+ <div class="navigation row btw">
+ <a href="<?= get_home_url() ?>">Home</a>
+ <?php
+ $privacy = get_privacy_policy_url();
+ if ($privacy !== '') { ?>
+ <a href="<?= $privacy ?>">Our Privacy Policy</a>
+ <?php } ?>
+ </div>
+ <?php
+ }
+ protected function renderHeader():void
+ {
+ ?>
+ <!DOCTYPE html>
+ <html <?php language_attributes(); ?>>
+ <head>
+ <title><?= $this->title ?> | <?= get_bloginfo('name') ?></title>
+ <meta charset="<?php bloginfo('charset'); ?>">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="preconnect" href="<?= get_home_url()?>"/>
+ <?php wp_head(); ?>
+ </head>
+ <body class="login">
+ <?php jvbAccessibility();?>
+ <header>
<?php
- $i = 1;
- $radio = '<input type="radio" id="user0" name="user_type" value="subscriber" required checked>
- <label for="user0"></label>';
- $descriptions = '';
- foreach (JVB_USER as $role => $config) {
- if (jvbCheck('can_register', $config)) {
- $radio .= '<input type="radio" id="user'.$i.'" name="user_type" value="'.$role.'" required';
- $radio .= ($role === 'enthusiast' && $this->fromFavourites()) ? 'checked' : '';
- $radio .= '><label for="user'.$i.'">'.jvbIcon($role, ['title' =>$config['label'], 'size'=>40]).'<h4>'.$config['label'].'</h4><p>';
- $radio .= $config['join_text']??'';
- $radio .= '</p></label>';
-
- $descriptions .= '<div class="user'.$i.'">'.is_array($config['join_description']) ? implode('', array_map(function ($item) { return '<p>'.$item.'</p>'; }, $config['join_description'])) : '<p>'.$config['join_description'].'</p>'.'</div>';
-
- $i++;
- }
- }
-
- echo $radio;
- echo $descriptions;
+ $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
+ $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
+ echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
+ <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
+ jvbIcon('light', ['title'=> 'Light Mode']).
+ jvbIcon('dark', ['title'=>'Dark Mode']).
+ '</span></label>';
?>
- <input type="radio" id="enthusiast" name="user_type" value="enthusiast" required <?= ($this->fromFavourites()) ? 'checked' : '' ?>>
- <label for="enthusiast"><?=jvbIcon('heart', ['title' =>'Enthusiast', 'size'=>40])?><h4>Enthusiast</h4><p>Start here.</p></label>
- <input type="radio" id="artist" name="user_type" value="artist" required>
- <label for="artist"><?=jvbIcon('tattoo', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label>
- <input type="radio" id="partner" name="user_type" value="partner" required>
- <label for="partner"><?=jvbIcon('partner', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label>
- <p class="enthusiast">Save your favourites. Get notified.</p>
- <p class="artist">Show off your work.</p>
- <p class="partner">Support the community.</p>
- </div>
+ <p class="title">
+ <a href="<?= get_home_url(); ?>" rel="home" title="Back to Site">
+ <?php
+ $icon = (int) get_option( 'site_icon' );
+ $out = '';
+ if ($icon > 0) {
+ $url = wp_get_attachment_image_url( $icon);
+ if ($url) {
+ $out = '<img src="'.$url.'">';
+ }
+ }
+ if ($out == '') {
+ $out =jvbIcon('home');
+ }
+ ?><?= $out ?>
+ </a>
+ </p>
+ </header>
+ <main>
+ <?php
+ }
- <!-- Enthusiast Fields -->
- <div class="field-group" data-type="enthusiast">
- <h4>Welcome to the scene.</h4>
- <p>Sign up with your email to:</p>
- <ul>
- <li>Save your favourites for easy access</li>
- <li>Get notified when your favourite artists add new content</li>
- <li>Stay in the loop with local flash days and events</li>
- <li>Discover styles and artists that match your vision</li>
- </ul>
- <p>
- <label for="enthusiast_first_name" class="required-field">First Name</label>
- <input type="text" id="enthusiast_first_name" name="enthusiast_first_name" class="input">
- </p>
- <p>
- <label for="enthusiast_email" class="required-field">Email</label>
- <input type="email" id="enthusiast_email" name="enthusiast_email" class="input">
- </p>
- <div><p><b>BONUS</b>: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.</p></div>
- </div>
+ protected function renderFooter():void
+ {
+ ?>
- <!-- Artist Fields -->
- <div class="field-group" data-type="artist">
- <h4>Welcome to the scene!</h4>
- <p>We'll start small, with the basics. Before your profile goes live, we need to verify:</p>
- <ul>
- <li>you are who you say you are</li>
- <li>you work at the shop you listed</li>
- <li>your certification</li>
- </ul>
- <p>
- <label for="artist_first_name" class="required-field">First Name</label>
- <input type="text" id="artist_first_name" name="artist_first_name" class="input">
- </p>
- <p>
- <label for="artist_last_name" class="required-field">Last Name</label>
- <input type="text" id="artist_last_name" name="artist_last_name" class="input">
- </p>
- <p>
- <label for="artist_email" class="required-field">Email</label>
- <input type="email" id="artist_email" name="artist_email" class="input">
- </p>
- <p>
- <label for="artist_shop" class="required-field">Shop</label>
- <select id="artist_shop" name="artist_shop" class="input">
- <option value="">Select a shop</option>
- <option value="other">Add New Shop</option>
- <?php foreach ($shops as $shop) : ?>
- <option value="<?= esc_attr($shop->term_id); ?>"><?= esc_html($shop->name); ?></option>
- <?php endforeach; ?>
- </select>
- </p>
- <p id="other_shop_field" style="display: none;">
- <label for="artist_shop_other" class="required-field">Shop Name</label>
- <input type="text" id="artist_shop_other" name="artist_shop_other" class="input" placeholder="Shop name">
- </p>
+ <footer class="col">
+ <?= $this->labels['footer'] ?>
+ <?= jvbLoadingScreen() ?>
+ <?= TaxonomySelector::outputSelectorModal() ?>
+ <?php
+ do_action('jvbLoginFooter');
+ ?>
+ <p>Made with ♡ by <a href="https://jakevan.ca/">JakeVan</a></p>
+ </footer>
- <p>
- <label for="artist_type" class="required-field">Type</label>
- <input type="radio" id="type-tattoo-artist" name="artist_type" value="tattoo-artist">
- <label for="type-tattoo-artist">Tattoo Artist</label>
- <input type="radio" id="type-piercer" name="artist_type" value="piercer">
- <label for="type-piercer">Piercer</label>
- <input type="radio" id="type-other" name="artist_type" value="other">
- <label for="type-other">Other</label>
- </p>
- <p>
- <label for="artist_city" class="required-field">City</label>
- <select id="artist_city" name="artist_city" class="input">
- <option value="">Select a city</option>
- <option value="other">Add New City</option>
- <?php foreach ($cities as $city) : ?>
- <option value="<?= esc_attr($city->term_id); ?>"><?= esc_html($city->name); ?></option>
- <?php endforeach; ?>
- </select>
- </p>
- <p id="other_city_field" style="display: none;">
- <label for="artist_city_other" class="required-field">City Name</label>
- <input type="text" id="artist_city_other" name="artist_city_other" class="input" placeholder="City">
- </p>
+ <?php wp_footer(); ?>
- <div class="file-upload-container">
- <label class="file-upload-label">Certification or Training Documents</label>
- <p><i>Optional</i> — If you've been certified in bloodborne pathogen safety, or any other tattoo safety course, pass along your certificate. This just eases the verification process.</p>
- <div class="file-upload-wrapper">
- <input type="file" name="certification_file" id="certification_file" accept=".jpg,.jpeg,.png,.gif,.pdf" data-max-size="<?= $this->max_file_size; ?>">
- <p class="file-upload-text">
- <strong>Click to upload</strong> or drag and drop<br>
- JPG, PNG, GIF or PDF (max. 5MB)
- </p>
- </div>
- <div class="file-preview">
- <div class="file-preview-content">
- <span class="file-preview-name"></span>
- <button type="button" class="file-preview-remove">Remove</button>
- </div>
- </div>
- <div class="file-error"></div>
- </div>
- <p>Once you click register:</p>
- <ul>
- <li>We'll start looking into your information (usually within 24-48 hours)</li>
- <li>You'll get a password reset email</li>
- <li>Upon setting your password, you can start filling in your profile - but it won't go live until we've verified your information.</li>
- </ul>
- <p>If you have any questions or concerns - or anything you'd like to follow up on - email us at get@edmonton.ink or message us on <a target="_blank" href="https://www.instagram.com/edmonton.ink/" title="@edmonton.ink on Instagram">Instagram</a>.</p>
- <div><p><b>BONUS</b>: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.</p></div>
- </div>
+ </body>
+ </html>
- <!-- Partner Fields -->
- <div class="field-group" data-type="partner">
- <h4>Howdy, partner!</h4>
- <p>We appreciate your interest!</p>
- <p>edmonton.ink is a great place to showcase what you do, whether you:</p>
- <ul>
- <li>provide goods or services that tattoo artists could use</li>
- <li>provide goods or services that are tattoo adjacent (such as art, merch, etc)</li>
- <li>provide goods or services that folks who love tattoos could also love</li>
- </ul>
+ <?php
+ }
- <p>We'll start with some basics, then we'll reach out to follow up (usually within 24-48 hours).</p>
- <p>
- <label for="partner_name" class="required-field">Contact Name</label>
- <input type="text" id="partner_name" name="partner_name" class="input">
- </p>
- <p>
- <label for="partner_email" class="required-field">Email</label>
- <input type="email" id="partner_email" name="partner_email" class="input">
- </p>
- <p>
- <label for="partner_business" class="required-field">Business Name</label>
- <input type="text" id="partner_business" name="partner_business" class="input">
- </p>
- <p>
- <label for="partner_website">Business Website</label>
- <input type="url" id="partner_website" name="partner_website" class="input">
- </p>
- <p>
- <label for="partner_description">Why would you be a good fit?</label>
- <textarea id="partner_description" name="partner_description" rows="8"></textarea>
- </p>
- <p><i>Note:</i> — you must have good standing in the tattoo community to stay a partner of edmonton.ink.</p>
- <p>If we receive multiple requests to terminate a partnership with you from member artists, we reserve the right to cancel your listings.</p>
- </div>
+ protected function addHiddenTokenFields(): void
+ {
+ foreach ($this->tokenHandlers as $priority => $handlers) {
+ foreach ($handlers as $token_key => $handler) {
+ if (isset($_GET[$token_key])) {
+ $value = sanitize_text_field($_GET[$token_key]);
+ echo '<input type="hidden" name="' . esc_attr($token_key) . '" value="' . esc_attr($value) . '">';
+ }
+ }
+ }
+
+ if (isset($_GET['email'])) {
+ echo '<input type="hidden" name="token_email" value="' . esc_attr(sanitize_email($_GET['email'])) . '">';
+ }
+ }
+
+ /*************************************************************************
+ AJAX HANDLERS
+ *************************************************************************/
+ public function handleAjaxLogin(): void
+ {
+ check_ajax_referer('jvb_login', '_wpnonce');
+
+ // Rate limiting
+ if (!$this->checkAjaxRateLimit('login')) {
+ wp_send_json_error([
+ 'message' => 'Too many attempts. Please wait a moment.',
+ 'code' => 'rate_limit'
+ ], 429);
+ }
+
+ // Duplicate submission check
+ if (!$this->checkRequestId()) {
+ wp_send_json_error([
+ 'message' => 'Duplicate request detected',
+ 'code' => 'duplicate_request'
+ ], 409);
+ }
+
+ $email = sanitize_email($_POST['user_email'] ?? '');
+ $password = $_POST['user_password'] ?? '';
+ $remember = !empty($_POST['remember_me']);
+
+ if (empty($email) || empty($password)) {
+ wp_send_json_error([
+ 'message' => 'Please fill in all fields',
+ 'field' => empty($email) ? 'user_email' : 'user_password',
+ 'code' => 'missing_fields'
+ ]);
+ }
+
+ // Verify Turnstile if enabled
+ if (!$this->verifyTurnstile()) {
+ wp_send_json_error([
+ 'message' => 'Security verification failed',
+ 'code' => 'turnstile_failed'
+ ]);
+ }
+
+ $user = get_user_by('email', $email);
+ if (!$user) {
+ wp_send_json_error([
+ 'message' => 'Unknown email address',
+ 'field' => 'user_email',
+ 'code' => 'invalid_email'
+ ]);
+ }
+
+ $user = wp_authenticate($user->user_login, $password);
+
+ if (is_wp_error($user)) {
+ wp_send_json_error([
+ 'message' => $user->get_error_message(),
+ 'field' => 'user_password',
+ 'code' => $user->get_error_code()
+ ]);
+ }
+
+ wp_clear_auth_cookie();
+ wp_set_current_user($user->ID);
+ wp_set_auth_cookie($user->ID, $remember);
+
+ do_action('wp_login', $user->user_login, $user);
+
+ $redirect = $_POST['redirect_to'] ?? home_url('/dash');
+ wp_send_json_success(['redirect' => $redirect]);
+ }
+
+ public function handleAjaxRegister(): void
+ {
+ check_ajax_referer('jvb_register', '_wpnonce');
+
+ // Rate limiting
+ if (!$this->checkAjaxRateLimit('register')) {
+ wp_send_json_error([
+ 'message' => 'Too many attempts. Please wait a moment.',
+ 'code' => 'rate_limit'
+ ], 429);
+ }
+
+ // Duplicate submission check
+ if (!$this->checkRequestId()) {
+ wp_send_json_error([
+ 'message' => 'Duplicate request detected',
+ 'code' => 'duplicate_request'
+ ], 409);
+ }
+
+ // Verify Turnstile
+ if (!$this->verifyTurnstile()) {
+ wp_send_json_error([
+ 'message' => 'Security verification failed',
+ 'code' => 'turnstile_failed'
+ ]);
+ }
+
+ $name = sanitize_text_field($_POST['name'] ?? '');
+ $email = sanitize_email($_POST['email'] ?? '');
+ $user_type = sanitize_text_field($_POST['user_select'] ?? 'subscriber');
+
+ // Spam prevention - if subscriber is selected and there are other options
+ if ($user_type === 'subscriber' && count(JVB_USER) > 0) {
+ $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false);
+ if (!empty($registerable)) {
+ wp_send_json_error([
+ 'message' => 'Please select a valid account type',
+ 'field' => 'user_select',
+ 'code' => 'invalid_user_type'
+ ]);
+ }
+ }
+
+ // Validate fields
+ if (empty($name)) {
+ wp_send_json_error([
+ 'message' => 'Name is required',
+ 'field' => 'name',
+ 'code' => 'missing_name'
+ ]);
+ }
+
+ if (empty($email)) {
+ wp_send_json_error([
+ 'message' => 'Email is required',
+ 'field' => 'email',
+ 'code' => 'missing_email'
+ ]);
+ }
+
+ // Check if role can register
+ if ($user_type !== 'subscriber') {
+ if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) {
+ wp_send_json_error([
+ 'message' => 'Invalid account type',
+ 'field' => 'user_select',
+ 'code' => 'invalid_user_type'
+ ]);
+ }
+ }
+
+ // Check if email exists
+ if (email_exists($email)) {
+ wp_send_json_error([
+ 'message' => 'Email already registered',
+ 'field' => 'email',
+ 'code' => 'duplicate_email'
+ ]);
+ }
+
+ // Create user
+ $user_id = wp_create_user($email, wp_generate_password(), $email);
+
+ if (is_wp_error($user_id)) {
+ wp_send_json_error([
+ 'message' => $user_id->get_error_message(),
+ 'code' => 'user_creation_failed'
+ ]);
+ }
+
+ // Update user data
+ wp_update_user([
+ 'ID' => $user_id,
+ 'display_name' => $name,
+ 'first_name' => $name
+ ]);
+
+ // 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);
+
+ // Check if needs approval
+ if (Features::forMembership()->has('memberVerified') &&
+ in_array($role, JVB_MEMBERSHIP['memberVerified'] ?? [])) {
+ $user->add_cap('skip_moderation', false);
+ update_user_meta($user_id, BASE . 'pending_approval', true);
+ }
+ }
+
+ // Save additional fields
+ update_user_meta($user_id, BASE . 'user_type', $user_type);
+
+ // Process additional fields from form
+ foreach ($_POST as $key => $value) {
+ if (in_array($key, ['name', 'email', 'action', '_wpnonce', 'request_id', 'user_select'])) {
+ continue;
+ }
+ update_user_meta($user_id, BASE . $key, sanitize_text_field($value));
+ }
+
+ // Handle token handlers
+ $this->processTokenHandlers($user_id, $email);
+
+ // Send welcome email with password setup link
+ $this->sendWelcomeEmail($user_id);
+
+ // Trigger registration action for other systems
+ do_action('jvbAfterUserRegistration', $user_id, $user_type, $_POST);
+
+ wp_send_json_success([
+ 'message' => 'Registration successful! Check your email.',
+ 'title' => $this->labels['successTitle'] ?? 'Success!',
+ 'description' => $this->labels['successDescription'] ?? 'Check your email for next steps',
+ 'user_id' => $user_id // Important for file upload dependencies!
+ ]);
+ }
+
+ public function handleAjaxLostPassword(): void
+ {
+ check_ajax_referer('jvb_lostpassword', '_wpnonce');
+
+ // Rate limiting
+ if (!$this->checkAjaxRateLimit('lostpassword')) {
+ wp_send_json_error([
+ 'message' => 'Too many attempts. Please wait a moment.',
+ 'code' => 'rate_limit'
+ ], 429);
+ }
+
+ $email = sanitize_email($_POST['user_email'] ?? '');
+
+ if (empty($email)) {
+ wp_send_json_error([
+ 'message' => 'Email required',
+ 'field' => 'user_email',
+ 'code' => 'missing_email'
+ ]);
+ }
+
+ // Verify Turnstile
+ if (!$this->verifyTurnstile()) {
+ wp_send_json_error([
+ 'message' => 'Security verification failed',
+ 'code' => 'turnstile_failed'
+ ]);
+ }
+
+ // Use WordPress's built-in function
+ $result = retrieve_password($email);
+
+ if (is_wp_error($result)) {
+ wp_send_json_error([
+ 'message' => $result->get_error_message(),
+ 'code' => $result->get_error_code()
+ ]);
+ }
+
+ wp_send_json_success(['message' => 'Check your email for reset link']);
+ }
+
+ public function handleAjaxResetPassword(): void
+ {
+ check_ajax_referer('jvb_resetpass', '_wpnonce');
+
+ // Rate limiting
+ if (!$this->checkAjaxRateLimit('resetpass')) {
+ wp_send_json_error([
+ 'message' => 'Too many attempts. Please wait a moment.',
+ 'code' => 'rate_limit'
+ ], 429);
+ }
+
+ $key = sanitize_text_field($_POST['key'] ?? $_GET['key'] ?? '');
+ $login = sanitize_text_field($_POST['login'] ?? $_GET['login'] ?? '');
+ $pass1 = $_POST['pass1'] ?? '';
+ $pass2 = $_POST['pass2'] ?? '';
+
+ if (empty($key) || empty($login)) {
+ wp_send_json_error([
+ 'message' => 'Invalid reset link',
+ 'code' => 'invalid_key'
+ ]);
+ }
+
+ if (empty($pass1) || empty($pass2)) {
+ wp_send_json_error([
+ 'message' => 'Please enter a password',
+ 'field' => empty($pass1) ? 'pass1' : 'pass2',
+ 'code' => 'missing_password'
+ ]);
+ }
+
+ if ($pass1 !== $pass2) {
+ wp_send_json_error([
+ 'message' => 'Passwords do not match',
+ 'field' => 'pass2',
+ 'code' => 'password_mismatch'
+ ]);
+ }
+
+ // Verify reset key
+ $user = check_password_reset_key($key, $login);
+
+ if (is_wp_error($user)) {
+ wp_send_json_error([
+ 'message' => 'Invalid or expired reset link',
+ 'code' => 'invalid_key'
+ ]);
+ }
+
+ // Reset password
+ reset_password($user, $pass1);
+
+ wp_send_json_success([
+ 'message' => 'Password reset successfully',
+ 'redirect' => home_url('/login')
+ ]);
+ }
+
+
+ /**********************************************************************
+ TOKEN PROCESSING
+ **********************************************************************/
+ protected function processTokenHandlers(int $user_id, string $email): void
+ {
+ foreach ($this->tokenHandlers as $priority => $handlers) {
+ foreach ($handlers as $token_key => $handler) {
+ if (isset($_POST[$token_key]) || isset($_GET[$token_key])) {
+ $token_value = $_POST[$token_key] ?? $_GET[$token_key];
+ call_user_func($handler, sanitize_text_field($token_value), $email, $user_id);
+ }
+ }
+ }
+ }
+
+
+ /***********************************************************************
+ EMAIL SENDING
+ ***********************************************************************/
+ protected function sendWelcomeEmail(int $user_id): void
+ {
+ $user = get_userdata($user_id);
+ if (!$user) {
+ return;
+ }
+
+ // Generate password reset key
+ $key = get_password_reset_key($user);
+ if (is_wp_error($key)) {
+ error_log('Failed to generate password reset key: ' . $key->get_error_message());
+ return;
+ }
+
+ $reset_url = add_query_arg([
+ 'action' => 'rp',
+ 'key' => $key,
+ 'login' => rawurlencode($user->user_login)
+ ], home_url('/login'));
+
+ $subject = $this->labels['email'] ?? 'Welcome to ' . get_bloginfo('name');
+
+ $message = '<h2>Welcome, ' . esc_html($user->display_name) . '!</h2>';
+ $message .= '<p>Your account has been created. Click the button below to set your password and get started:</p>';
+ $message .= jvbMailButton($reset_url, 'Set Your Password');
+ $message .= '<p>This link expires in 24 hours.</p>';
+
+ $this->emailManager->sendEmail($user->user_email, $subject, $message);
+ }
+
+ /*************************************************************************
+ * SECURITY & VALIDATION
+ *************************************************************************/
+ protected function checkAjaxRateLimit(string $action): bool
+ {
+ return $this->rateLimiter->checkLimit($action);
+ }
+ protected function checkRequestId(): bool
+ {
+ $request_id = $_POST['request_id'] ?? '';
+ if (empty($request_id)) {
+ return true; // No request_id provided, allow (for backward compat)
+ }
+
+ $cache_key = 'request_' . $request_id;
+ if (get_transient($cache_key)) {
+ return false; // Duplicate request
+ }
+
+ // Store request ID for 1 minute to prevent duplicates
+ set_transient($cache_key, true, 60);
+ return true;
+ }
+
+ protected function maybeTurnstile(): void
+ {
+ if (!Features::hasIntegration('cloudflare')) {
+ return;
+ }
+ JVB()->connect('cloudflare')->renderTurnstile();
+ }
+
+ protected function maybeTurnstileScripts(): void
+ {
+ if (!Features::hasIntegration('cloudflare')) {
+ return;
+ }
+ JVB()->connect('cloudflare')->enqueueTurnstileScripts();
+ }
+
+ protected function verifyTurnstile(): bool
+ {
+ if (!Features::hasIntegration('cloudflare')) {
+ return true; // Not enabled, pass verification
+ }
+
+ $token = $_POST['cf-turnstile-response'] ?? '';
+ if (empty($token)) {
+ return false;
+ }
+
+ return JVB()->connect('cloudflare')->verifyTurnstile($token);
+ }
+
+ /************************************************************************
+ LABELS & UI
+ ************************************************************************/
+ protected function setupLabels(): void
+ {
+ $default = $this->getDefaultLabels();
+ $this->labels = apply_filters('jvbLoginLabels', $default, $_GET);
+
+ foreach (['description', 'footer', 'extra'] as $location) {
+ $text = (!is_array($this->labels[$location])) ? [$this->labels[$location]] : $this->labels[$location];
+ if (!empty($text)) {
+ $this->labels[$location] = '<div class="'.$location.'">';
+ foreach ($text as $d) {
+ $this->labels[$location] .= '<p>'.$d.'</p>';
+ }
+ $this->labels[$location] .= '</div>';
+ }
+ }
+ }
+
+ protected function getDefaultLabels(): array
+ {
+ switch ($this->action) {
+ case 'register':
+ return [
+ 'title' => JVB_LOGIN['register']['title'] ?? 'Create Your Account',
+ 'description' => JVB_LOGIN['register']['description'] ?? [],
+ 'extra' => JVB_LOGIN['register']['extra'] ?? [],
+ 'footer' => JVB_LOGIN['register']['footer'] ?? '',
+ 'email' => JVB_LOGIN['register']['email']['subject'] ?? '['.get_bloginfo('name').'] Finish Creating Your Account',
+ 'submit' => JVB_LOGIN['register']['submit'] ?? 'Create Account',
+ 'successTitle' => JVB_LOGIN['register']['success']['title'] ?? 'Success!',
+ 'successDescription' => JVB_LOGIN['register']['success']['description'] ?? ['See your email for next steps','(Check your spam folder if you cannot find it after a couple minutes.)'],
+ ];
+ case 'lostpassword':
+ return [
+ 'title' => JVB_LOGIN['forgot_password']['title'] ?? 'Reset Password',
+ 'description' => JVB_LOGIN['forgot_password']['description'] ?? [],
+ 'extra' => JVB_LOGIN['forgot_password']['extra'] ?? [],
+ 'footer' => JVB_LOGIN['forgot_password']['footer'] ?? '',
+ 'submit' => JVB_LOGIN['forgot_password']['submit'] ?? 'Send Reset Link',
+ 'successTitle' => JVB_LOGIN['forgot_password']['success']['title'] ?? 'Success!',
+ 'successDescription' => JVB_LOGIN['forgot_password']['success']['description'] ?? ['Check your email for reset instructions'],
+ ];
+ case 'resetpass':
+ return [
+ 'title' => JVB_LOGIN['reset_pass']['title'] ?? 'Reset Your Password',
+ 'description' => JVB_LOGIN['reset_pass']['description'] ?? [],
+ 'extra' => JVB_LOGIN['reset_pass']['extra'] ?? [],
+ 'footer' => JVB_LOGIN['reset_pass']['footer'] ?? '',
+ 'submit' => JVB_LOGIN['reset_pass']['submit'] ?? 'Reset Password',
+ ];
+ case 'login':
+ default:
+ return [
+ 'title' => JVB_LOGIN['login']['title'] ?? 'Sign in',
+ 'description' => JVB_LOGIN['login']['description'] ?? [],
+ 'extra' => JVB_LOGIN['login']['extra'] ?? [],
+ 'footer' => JVB_LOGIN['login']['footer'] ?? '',
+ 'submit' => JVB_LOGIN['login']['submit'] ?? 'Sign In',
+ ];
+ }
+ }
+
+ protected function maybeMagicLink(): void
+ {
+ if (!$this->magicLink || !in_array($this->action, ['login', 'lostpassword'])) {
+ return;
+ }
+ ?>
+ <button type="button" id="magic-link-btn" class="button button-secondary button-large">
+ <?= jvbIcon('email', ['size' => 20]); ?>
+ Get Login Link
+ </button>
+ <script type="text/javascript">
+ document.getElementById('magic-link-btn')?.addEventListener('click', function(e) {
+ e.preventDefault();
+ const email = document.querySelector('input[name="user_email"]')?.value;
+ if (!email) {
+ alert('Please enter your email address first');
+ return;
+ }
+
+ fetch('<?= rest_url('jvb/v1/magic-link'); ?>', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': '<?= wp_create_nonce('wp_rest') ?>'
+ },
+ body: JSON.stringify({ email: email, type: 'login' })
+ })
+ .then(r => r.json())
+ .then(data => {
+ alert(data.success ? 'Check your email!' : (data.message || 'Failed to send link'));
+ });
+ });
+ </script>
<?php
}
- /**
- * Registration errors filter
- */
- public function registrationErrorsFilter(WP_Error $errors, string $sanitized_user_login, string $user_email): WP_Error
- {
- error_log('Registration Data: '.print_r($_POST, true));
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
- if (empty($user_type)) {
- $errors->add('user_type_error', 'Please select your user type.');
- return $errors;
- }
+ /************************************************************************
+ SCRIPTS
+ ************************************************************************/
+ public function enqueueScripts(): void
+ {
+ if (!$this->isLoginPage()) {
+ return;
+ }
- // Get email based on user type
- $email_field = $user_type . '_email';
- $email = isset($_POST[$email_field]) ? sanitize_email($_POST[$email_field]) : '';
+ $this->maybeTurnstileScripts();
+ wp_enqueue_script('jvb-form');
- // Remove WordPress's default username error
- $errors = new WP_Error();
+ $script = "
+ document.addEventListener('DOMContentLoaded', () => {
+ const form = document.querySelector('.login form');
+ if (form && window.jvbForm) {
+ let controller = new window.jvbForm();
+ controller.registerForm(form, {
+ autosave: false,
+ endpoint: false
+ });
+ } else if (form && !window.jvbForm) {
+ console.error('jvbForm not loaded');
+ }
+ });";
- // If this is an invited artist, validate the invitation
- $invite = (array_key_exists('invite_token', $_POST)) ? sanitize_text_field($_POST['invite_token']) : false;
- if ($invite && array_key_exists('role', $_POST)) {
- $handler = JVB()->routes('invites');
- $invitation = $handler->verifyInvitation($invite, sanitize_email($_POST['invite_email']), sanitize_text_field($_POST['role']));
+ wp_add_inline_script('jvb-form', $script);
+ }
- if (!$invitation) {
- $errors->add('invalid_invitation', 'Invalid invitation token.');
- } elseif (strtotime($invitation->expires_at) < current_time('timestamp')) {
- $errors->add('expired_invitation', 'This invitation has expired.');
- }
- }
+ /*************************************************************************
+ SUCCESS HANDLING
+ *************************************************************************/
+ public function handleSuccessfulLogin(string $username, WP_User $user): void
+ {
+ if (isOurPeople() && !user_can($user, 'manage_options')) {
+ wp_redirect(get_home_url(null, '/dash'));
+ exit;
+ }
+ }
- // Validate email first
- if (empty($email)) {
- $errors->add('email_error', 'Email is required.');
- } elseif (!is_email($email)) {
- $errors->add('email_error', 'Please enter a valid email address.');
- } elseif (email_exists($email)) {
- $errors->add('email_error', 'This email is already registered.');
- }
- switch ($user_type) {
- case 'enthusiast':
- if (empty($_POST['enthusiast_first_name'])) {
- $errors->add('first_name_error', 'First name is required.');
- }
- break;
+ /**
+ * Handle login errors
+ */
+ protected function handleLoginError(WP_Error $error): void
+ {
+ $login_url = wp_login_url();
+ $login_url = add_query_arg('login_error', urlencode($error->get_error_code()), $login_url);
- case 'artist':
- $required_fields = array(
- 'artist_first_name' => 'First name',
- 'artist_last_name' => 'Last name',
- 'artist_shop' => 'Shop',
- 'artist_city' => 'City',
- 'artist_type' => 'Type',
- );
- foreach ($required_fields as $field => $label) {
- if (empty($_POST[$field])) {
- $errors->add($field . '_error', $label . ' is required.');
- }
- }
- break;
+ if (isset($_REQUEST['redirect_to'])) {
+ $login_url = add_query_arg('redirect_to', urlencode($_REQUEST['redirect_to']), $login_url);
+ }
- case 'partner':
- $required_fields = array(
- 'partner_name' => 'Contact name',
- 'partner_business' => 'Business name'
- );
-
- foreach ($required_fields as $field => $label) {
- if (empty($_POST[$field])) {
- $errors->add($field . '_error', $label . ' is required.');
- }
- }
- break;
- }
-
- if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) {
- $file = $_FILES['certification_file'];
-
- // Validate file type
- if (!in_array($file['type'], $this->allowed_file_types)) {
- $errors->add('file_type_error', 'Please upload a valid file type (JPG, PNG, GIF, or PDF)');
- }
-
- // Validate file size
- if ($file['size'] > $this->max_file_size) {
- $errors->add('file_size_error', 'File size must be less than 5MB');
- }
- }
-
- return $errors;
- }
-
- /**
- * Save registration fields
- */
- public function saveRegistrationFields(int $user_id, array $userdata): void
- {
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : false;
- if (!$user_type) {
- return;
- }
-
- // Set user role based on type
- $user = new WP_User($user_id);
- $caps = JVB()->roles();
- $email = false;
- $upload_dir = wp_upload_dir();
- $base_dir = $upload_dir['basedir'];
-
- switch ($user_type) {
- case 'artist':
- $user->set_role('jvb_artist');
- $user->remove_role('subscriber');
-
- $email = sanitize_email($_POST['artist_email']);
- $first = sanitize_text_field($_POST['artist_first_name']);
- $last = sanitize_text_field($_POST['artist_last_name']);
- $display_name = $first . ' ' . $last;
-
- // Save artist fields
- $temp = wp_update_user([
- 'ID' => $user_id,
- 'first_name' => $first,
- 'last_name' => $last,
- 'display_name' => $display_name
- ]);
- $user = get_userdata($temp);
-
- $link = $caps->addUserLink($user, 'artist');
- $meta = new MetaManager($link, 'post');
- $meta->setAll([
- 'first_name' => $first,
- 'email' => $email
- ]);
-
- // If this was an invited artist, handle the invitation
- if (array_key_exists('invite_token', $_POST)) {
- $handler = JVB()->routes('invites');
- $handler->acceptInvitation(sanitize_text_field($_POST['invite_token']), sanitize_email($_POST['invite_email']), $user->ID);
- }
-
- if (absint($_POST['artist_shop']) > 0) {
- JVB()->routes('shop')->requestShopAdmission($user_id, absint($_POST['artist_shop']));
- }
- if (absint($_POST['artist_city']) > 0) {
- wp_set_post_terms($link, (int)absint($_POST['artist_city']), BASE.'city');
- }
-
- //Create approval request and notify verified users
- JVB()->routes('approvals')->createArtistApprovalRequest($user_id);
-
- //Make base directories
- $artist_dir = $base_dir . '/artists/' . $user_id;
- wp_mkdir_p($artist_dir);
- wp_mkdir_p($artist_dir . '/artwork');
- wp_mkdir_p($artist_dir . '/events');
- wp_mkdir_p($artist_dir . '/profile');
- wp_mkdir_p($artist_dir . '/temp');
-
- switch ($_POST['artist_type']) {
- case 'tattoo-artist':
- $caps->setUserAs($user, 'tattoo-artist');
- $term = get_term_by('name', 'Tattoo Artists', BASE.'type');
- if ($term && !is_wp_error($term)) {
- wp_set_post_terms($link, $term->term_id, BASE.'type');
- }
- wp_mkdir_p($artist_dir . '/tattoos');
- break;
- case 'piercer':
- $caps->setUserAs($user, 'piercer');
- $term = get_term_by('name', 'Piercers', BASE.'type');
- if ($term && !is_wp_error($term)) {
- wp_set_post_terms($link, $term->term_id, BASE.'type');
- }
- wp_mkdir_p($artist_dir . '/piercings');
- break;
- }
- break;
-
- case 'partner':
- $user->set_role('jvb_partner');
- $user->remove_role('subscriber');
- $name = sanitize_text_field($_POST['partner_name']);
- $email = sanitize_email($_POST['partner_email']);
-
- $caps->setUserAs($user, 'partner');
- $link = $caps->addUserLink($user, 'partner');
-
- // Save partner fields
- update_user_meta($user_id, 'contact_name', sanitize_text_field($_POST['partner_name']));
- update_user_meta($user_id, 'business_name', sanitize_text_field($_POST['partner_business']));
- update_user_meta($user_id, 'business_website', esc_url_raw($_POST['partner_website']));
-
- // Create partner base directory
- $partner_dir = $base_dir . '/partners/' . $user_id;
- wp_mkdir_p($partner_dir);
- wp_mkdir_p($partner_dir . '/offers');
- wp_mkdir_p($partner_dir . '/events');
- wp_mkdir_p($partner_dir . '/profile');
- wp_mkdir_p($partner_dir . '/temp');
- break;
-
- case 'enthusiast':
- $user->set_role('jvb_enthusiast');
- $user->remove_role('subscriber');
- $caps->setUserAs($user, 'enthusiast');
- $name = sanitize_text_field($_POST['enthusiast_first_name']);
- $email = sanitize_email($_POST['enthusiast_email']);
-
- // Save enthusiast fields
- $temp = wp_update_user([
- 'ID' => $user_id,
- 'first_name' => $name,
- 'user_email' => $email,
- ]);
- break;
- }
-
- // Handle file upload for artists
- if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) {
- $file = $_FILES['certification_file'];
-
- // Setup upload directory
- $upload_dir = wp_upload_dir();
- $user_directory = 'artist-certifications/' . $user_id;
- $target_dir = $upload_dir['basedir'] . '/' . $user_directory;
-
- // Create directory if it doesn't exist
- wp_mkdir_p($target_dir);
-
- // Generate unique filename
- $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
- $filename = 'certification-' . time() . '.' . $file_extension;
- $target_file = $target_dir . '/' . $filename;
-
- // Move uploaded file
- if (move_uploaded_file($file['tmp_name'], $target_file)) {
- // Save file information in user meta
- update_user_meta($user_id, 'certification_file', array(
- 'url' => $upload_dir['baseurl'] . '/' . $user_directory . '/' . $filename,
- 'file' => $target_file,
- 'type' => $file['type'],
- 'original_name' => $file['name']
- ));
- }
- }
-
- // Handle list invitation acceptance
- if (isset($_GET['list_token']) && !empty($_GET['list_token']) && isset($_GET['email'])) {
- $token = sanitize_text_field($_GET['list_token']);
- $email = sanitize_email($_GET['email']);
-
- if ($email) {
- JVB()->routes('favourites')->acceptListInvitation($token, $email, $user_id);
- }
- }
- }
-
- /**
- * Registration success message
- */
- public function registrationSuccessMessage(WP_Error $errors, string $redirect_to): WP_Error
- {
- if (isset($errors->errors['registered']) && isset($_POST['invitation_token'])) {
- // Custom message for invited artists
- $message = "WELCOME ABOARD!<br><br>" .
- "Password setup is in your inbox. <br>" .
- "Since you were invited by a shop, you can skip the verification wait and start building your profile right away! ♡";
-
- unset($errors->errors['registered']);
- $errors->add('registered', $message, 'message');
- }
-
- if (isset($errors->errors['registered'])) {
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : 'user';
-
- switch ($user_type) {
- case 'enthusiast':
- $message = "YOU'RE IN!<br><br>Check your inbox - we've sent password setup details.<br>Get ready to build your dream artist collection! ♡";
- break;
-
- case 'artist':
- $message = "HELL YEAH!<br><br>Password setup is in your inbox. <br>While we verify your info (24-48hrs), you can start building your profile. <br>Just remember - it stays underground until you're cleared. ♡";
- break;
-
- case 'partner':
- $message = "ROCK ON!<br><br>Check your inbox - we've sent password setup details.<br>We'll check out your pitch in the next 24-48hrs. <br><br>Meanwhile, you can start prepping your presence - but you won't hit the streets until we give the nod. ♡";
- break;
-
- default:
- $message = "YOU'RE ON THE LIST!<br><br>Check your inbox for the next steps. ♡";
- }
-
- // Replace the default message
- unset($errors->errors['registered']);
- $errors->add('registered', $message, 'message');
- }
-
- return $errors;
- }
-
- /**
- * Check if registration is from invite
- */
- protected function fromInvite(): bool
- {
- return isset($_GET['invite']) && isset($_GET['email']);
- }
-
- /**
- * Custom register message
- */
- public function customRegisterMessage(string $message): string
- {
- return "Join Edmonton's tattoo community";
- }
+ wp_safe_redirect($login_url);
+ exit;
+ }
}
-// Initialize the consolidated auth manager
+// Initialize the login manager
new LoginManager();
--
Gitblit v1.10.0