| | |
| | | <?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\meta\Form; |
| | | |
| | | use JVBase\utility\Features; |
| | | use WP_Error; |
| | | use WP_User; |
| | |
| | | class LoginManager |
| | | { |
| | | protected Features $siteFeatures; |
| | | protected ?MagicLinkManager $magicLink = null; |
| | | protected ?MetaForm $metaForm = null; |
| | | protected EmailManager $emailManager; |
| | | protected AjaxRateLimiter $rateLimiter; |
| | | protected CacheManager $cache; |
| | | protected ?Form $form = null; |
| | | protected Cache $cache; |
| | | |
| | | |
| | | protected array $forms =[]; |
| | |
| | | public function __construct() |
| | | { |
| | | $this->siteFeatures = Features::forSite(); |
| | | $this->emailManager = new EmailManager(); |
| | | |
| | | |
| | | $this->cache = CacheManager::for('login'); |
| | | $this->cache = Cache::for('login'); |
| | | |
| | | // Initialize magic link support if enabled |
| | | if ($this->siteFeatures->has('magicLink')) { |
| | |
| | | // Login success handling |
| | | add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2); |
| | | |
| | | add_filter('lostpassword_url', [$this, 'resetPasswordUrl'], 10, 2); |
| | | add_filter( 'login_url', [$this, 'loginUrl'], 10, 3 ); |
| | | add_filter( 'logout_url', [$this, 'logoutUrl'], 10, 2 ); |
| | | // Allow other features to register handlers |
| | | do_action('jvbLoginManagerInit', $this); |
| | | add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2); |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1); |
| | | } |
| | | |
| | | public function excludeLoginSitemap(array $ids): array |
| | | { |
| | | $ids[] = $this->getLoginPage(); |
| | | return $ids; |
| | | } |
| | | /************************************************************************** |
| | | * SETUP & CONFIGURATION |
| | | **************************************************************************/ |
| | |
| | | return; |
| | | } |
| | | // Build custom login URL with all query args |
| | | $custom_login_page = home_url('/login'); |
| | | $custom_login_page = home_url('/login/'); |
| | | $query_args = $_GET; |
| | | |
| | | // Remove WordPress internal args |
| | |
| | | 'type' => 'email', |
| | | 'label' => __('Email Address', 'jvb'), |
| | | 'required' => true, |
| | | 'autocomplete' => 'email', |
| | | 'placeholder' => 'look@me.com', |
| | | ], |
| | | 'user_password' => [ |
| | | 'type' => 'text', |
| | | 'subtype'=> 'password', |
| | | 'label' => __('Password', 'jvb'), |
| | | 'autocomplete' => 'current-password', |
| | | 'required' => true, |
| | | ], |
| | | 'remember_me' => [ |
| | |
| | | } |
| | | } |
| | | } |
| | | public function loginUrl(string $login_url, string $redirect, bool $force_reauth):string |
| | | { |
| | | // This will append /custom-login/ to you main site URL as configured in general settings (ie https://domain.com/custom-login/) |
| | | $login_url = site_url( '/login/', 'login' ); |
| | | if ( ! empty( $redirect ) ) { |
| | | $login_url = add_query_arg( 'redirect_to', urlencode( $redirect ), $login_url ); |
| | | } |
| | | if ( $force_reauth ) { |
| | | $login_url = add_query_arg( 'reauth', '1', $login_url ); |
| | | } |
| | | return $login_url; |
| | | } |
| | | |
| | | public function logoutUrl(string $logout_url, string $redirect): string |
| | | { |
| | | // Build custom logout URL |
| | | $logout_url = site_url('/login/', 'login'); |
| | | $logout_url = add_query_arg('action', 'logout', $logout_url); |
| | | |
| | | if (!empty($redirect)) { |
| | | $logout_url = add_query_arg('redirect_to', urlencode($redirect), $logout_url); |
| | | } |
| | | |
| | | // Add nonce for security |
| | | return wp_nonce_url($logout_url, 'log-out'); |
| | | } |
| | | public function resetPasswordUrl(string $url, string $redirect):string |
| | | { |
| | | error_log('reset Password Url:'.print_r($url, true)); |
| | | error_log('reset password redirect: '.print_r($redirect, true)); |
| | | |
| | | return str_replace('wp_login.php', 'login/', $url); |
| | | |
| | | } |
| | | public function getLoginPage():int|false |
| | | { |
| | | return (int)get_option(BASE.'login_page'); |
| | |
| | | if (!Features::forSite()->has('magicLink')) { |
| | | return; |
| | | } |
| | | $this->magicLink = new MagicLinkManager(); |
| | | } |
| | | |
| | | /********************************************************************* |
| | |
| | | protected function setup():void |
| | | { |
| | | $this->action = $this->getAction(); |
| | | if (in_array($this->action, ['logout']) || array_key_exists('loggedout', $_GET)) { |
| | | if ($this->action == 'logout' || array_key_exists('loggedout', $_GET)) { |
| | | wp_logout(); |
| | | wp_redirect(esc_attr($_GET['redirect_to'] ?? get_home_url())); |
| | | exit; |
| | | } |
| | | if (in_array($this->action, ['rp', 'resetpass']) && !is_user_logged_in()) { |
| | | wp_redirect(wp_login_url()); |
| | | exit; |
| | | } elseif (is_user_logged_in()) { |
| | | wp_redirect(get_home_url(null, '/dash/')); |
| | | } |
| | | $this->setupLabels(); |
| | | $this->setupFields(); |
| | | $this->setupTitle(); |
| | |
| | | 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]; |
| | | |
| | | $small = wp_get_attachment_image_src($logo, 'medium')[0]??''; |
| | | $large = wp_get_attachment_image_src($logo, 'large')[0]??''; |
| | | } |
| | | echo '<style> |
| | | .login header, |
| | |
| | | |
| | | protected function renderForms():void |
| | | { |
| | | $this->metaForm = new MetaForm(); |
| | | $form = $this->action.'form'; |
| | | ?> |
| | | <section class="login-box col btw"> |
| | |
| | | do_action('jvb_add_token_inputs', $this->action); |
| | | |
| | | foreach ($this->fields as $name => $config) { |
| | | $this->metaForm->render($name, '', $config); |
| | | echo Form::render($name, '', $config); |
| | | } |
| | | |
| | | $this->maybeTurnstile(); |
| | |
| | | $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">'. |
| | | <span class="screen-reader-text">Toggle dark mode</span> |
| | | <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'. |
| | | jvbIcon('sun-dim', ['title'=> 'Light Mode']). |
| | | jvbIcon('moon', ['title'=>'Dark Mode']). |
| | | '</span></label>'; |
| | |
| | | 'successDescription' => JVB_LOGIN['forgot_password']['success']['description'] ?? ['Check your email for reset instructions'], |
| | | ]; |
| | | case 'resetpass': |
| | | case 'rp': |
| | | return [ |
| | | 'title' => JVB_LOGIN['reset_pass']['title'] ?? 'Reset Your Password', |
| | | 'description' => JVB_LOGIN['reset_pass']['description'] ?? [], |
| | |
| | | |
| | | protected function maybeMagicLink(): void |
| | | { |
| | | if (!$this->magicLink || !in_array($this->action, ['login', 'lostpassword'])) { |
| | | if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) { |
| | | return; |
| | | } |
| | | ?> |
| | |
| | | SCRIPTS |
| | | ************************************************************************/ |
| | | public function enqueueScripts(): void |
| | | { |
| | | if (!$this->isLoginPage()) { |
| | | return; |
| | | } |
| | | { |
| | | if (!$this->isLoginPage()) { |
| | | return; |
| | | } |
| | | |
| | | $this->maybeTurnstileScripts(); |
| | | wp_enqueue_script('jvb-form'); |
| | | $action = $this->getAction(); |
| | | ob_start(); |
| | | ?> |
| | | $this->maybeTurnstileScripts(); |
| | | wp_enqueue_script('jvb-form'); |
| | | $action = $this->getAction(); |
| | | |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | const form = document.querySelector('.login form'); |
| | | if (!form) return; |
| | | $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : ''; |
| | | $has_turnstile = Features::hasIntegration('cloudflare'); |
| | | |
| | | if (!window.jvbForm) { |
| | | console.error('jvbForm not loaded'); |
| | | return; |
| | | } |
| | | ob_start(); |
| | | |
| | | window.LoginController = new window.jvbForm(); |
| | | window.LoginController.registerForm(form, { |
| | | autosave: false, |
| | | endpoint: <?= "'{$action}'" ?>, |
| | | formStatus: false, |
| | | cache: false, |
| | | }); |
| | | ?> |
| | | |
| | | window.LoginController.subscribe((event, data) => { |
| | | if (event === 'form-submit') { |
| | | handleFormSubmission(data); |
| | | } |
| | | }); |
| | | document.addEventListener('DOMContentLoaded', async function () { |
| | | const hasTurnstile = <?= json_encode($has_turnstile) ?>; |
| | | const redirectTo = <?= json_encode($redirect_to) ?>; |
| | | |
| | | window.auth.subscribe(event => { |
| | | if (event === 'auth-loaded') { |
| | | const form = document.querySelector('.login form'); |
| | | if (!form || !window.jvbForm) return; |
| | | |
| | | async function handleFormSubmission(data) { |
| | | let realFormData = data.fullData; |
| | | const { formId, config, data: formData } = data; |
| | | |
| | | |
| | | const form = config.element; |
| | | |
| | | const submit = form.querySelector('[type=submit]'); |
| | | let oldText = submit.textContent; |
| | | |
| | | |
| | | window.LoginController.showFormStatus(formId, 'uploading'); |
| | | |
| | | try { |
| | | submit.disabled = true; |
| | | submit.textContent = 'Loading...'; |
| | | const response = await fetch(`${jvbSettings.api}<?=($action === 'magic') ? $action : 'auth/'.$action?>`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': jvbSettings.nonce |
| | | }, |
| | | body: JSON.stringify(realFormData) |
| | | window.jvbForm.registerForm(form, { |
| | | autosave: false, |
| | | endpoint: '<?= $action ?>', |
| | | formStatus: false, |
| | | cache: false, |
| | | }); |
| | | |
| | | const result = await response.json(); |
| | | window.jvbForm.subscribe((event, data) => { |
| | | if (event === 'form-submit') { |
| | | const { config } = data; |
| | | const formElement = config.element; |
| | | |
| | | // Handle errors |
| | | if (!response.ok) { |
| | | window.LoginController.showFormStatus(formId, 'error'); |
| | | window.LoginController.handleFormError(form, result); |
| | | return; |
| | | // Collect current form data |
| | | const formData = new FormData(formElement); |
| | | const formObject = Object.fromEntries(formData.entries()); |
| | | |
| | | let params = new URLSearchParams(window.location.search); |
| | | if (params.has('key')) { |
| | | formObject['key'] = params.get('key'); |
| | | } |
| | | if (params.has('login')) { |
| | | formObject['login'] = params.get('login'); |
| | | } |
| | | |
| | | // Add redirect_to from URL |
| | | if (redirectTo) { |
| | | formObject.redirect_to = redirectTo; |
| | | } |
| | | |
| | | const submit = formElement.querySelector('[type=submit]'); |
| | | const oldText = submit.textContent; |
| | | |
| | | window.jvbForm.showFormStatus(config.id, 'uploading'); |
| | | |
| | | submit.disabled = true; |
| | | submit.textContent = 'Loading...'; |
| | | |
| | | window.auth.fetch(`${jvbSettings.api}auth/<?= $action ?>`, { |
| | | method: 'POST', |
| | | body: JSON.stringify(formObject) |
| | | }) |
| | | .then(response => response.json().then(result => ({ response, result }))) |
| | | .then(({ response, result }) => { |
| | | if (!response.ok) { |
| | | window.jvbForm.showFormStatus(config.id, 'error'); |
| | | window.jvbForm.handleFormError(formElement, result); |
| | | return; |
| | | } |
| | | |
| | | window.jvbForm.showFormStatus(config.id, 'submitted'); |
| | | |
| | | if (result.message) { |
| | | window.jvbForm.handleFormSuccess(formElement, result); |
| | | } |
| | | |
| | | if (window.auth?.handleLogin && result.auth) { |
| | | return window.auth.handleLogin(result.auth).then(() => { |
| | | if (result.redirect) { |
| | | setTimeout(() => { |
| | | window.location.href = result.redirect; |
| | | }, 100); |
| | | } |
| | | }); |
| | | } else if (result.redirect) { |
| | | setTimeout(() => { |
| | | window.location.href = result.redirect; |
| | | }, 100); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error('Form submission error:', error); |
| | | window.jvbForm.showFormStatus(config.id, 'error'); |
| | | window.jvbForm.handleFormError(formElement, { |
| | | message: 'Network error. Please check your connection and try again.', |
| | | code: 'network_error' |
| | | }); |
| | | }) |
| | | .finally(() => { |
| | | submit.textContent = oldText; |
| | | submit.disabled = false; |
| | | }); |
| | | } |
| | | |
| | | // Handle success |
| | | window.LoginController.showFormStatus(formId, 'submitted'); |
| | | |
| | | // Show success message briefly before redirect |
| | | if (result.message) { |
| | | window.LoginController.handleFormSuccess(form, result); |
| | | } |
| | | |
| | | // Handle redirect |
| | | if (result.redirect) { |
| | | setTimeout(() => { |
| | | window.location.href = result.redirect; |
| | | }, 500); // Brief delay to show success message |
| | | } |
| | | |
| | | } catch (error) { |
| | | console.error('Form submission error:', error); |
| | | window.LoginController.showFormStatus(formId, 'error'); |
| | | window.LoginController.handleFormError(form, { |
| | | message: 'Network error. Please check your connection and try again.', |
| | | code: 'network_error' |
| | | }); |
| | | } finally { |
| | | submit.textContent = oldText; |
| | | submit.disabled = false; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | }); |
| | | }); |
| | | |
| | | |
| | | |
| | | <?php |
| | | <?php |
| | | $script = ob_get_clean(); |
| | | |
| | | wp_add_inline_script('jvb-form', $script); |
| | | } |
| | | |
| | |
| | | wp_safe_redirect($login_url); |
| | | exit; |
| | | } |
| | | |
| | | public function saveRegistrationFields(int $user_id, array $userdata):void |
| | | { |
| | | |
| | | } |
| | | } |
| | | |
| | | // Initialize the login manager |