| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\forms\TaxonomySelector; |
| | | use JVBase\meta\Form; |
| | | |
| | | use JVBase\utility\Features; |
| | | use WP_Error; |
| | | use JVBase\registrar\Registrar;use WP_Error; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | class LoginManager |
| | | { |
| | | protected Features $siteFeatures; |
| | | protected ?Form $form = null; |
| | | protected Cache $cache; |
| | | |
| | |
| | | protected array $fields = []; |
| | | protected ?string $action = null; |
| | | protected string $title = ''; |
| | | protected static LoginManager $instance; |
| | | |
| | | // Token handlers registry |
| | | protected array $messageHandlers = []; |
| | |
| | | ]; |
| | | private int $max_file_size = 5242880; // 5MB in bytes |
| | | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->siteFeatures = Features::forSite(); |
| | | |
| | | |
| | | self::$instance = $this; |
| | | $this->cache = Cache::for('login'); |
| | | |
| | | $this->cache->flush(); |
| | | // Initialize magic link support if enabled |
| | | if ($this->siteFeatures->has('magicLink')) { |
| | | if (Site::has('magicLink')) { |
| | | $this->initMagicLinkSupport(); |
| | | } |
| | | |
| | |
| | | add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2); |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1); |
| | | } |
| | | public static function getInstance():self |
| | | { |
| | | return self::$instance; |
| | | } |
| | | |
| | | public function excludeLoginSitemap(array $ids): array |
| | | { |
| | |
| | | 'placeholder'=> 'look@me.com' |
| | | ] |
| | | ]; |
| | | if (Features::forSite()->has('referrals')) { |
| | | if (Site::has('referrals')) { |
| | | $fields['referral_code'] = [ |
| | | 'type' => 'text', |
| | | 'required'=> false, |
| | |
| | | 'hint' => 'Have a referral code? Paste it here!' |
| | | ]; |
| | | } |
| | | 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'] ?? ''; |
| | | $canRegister = Registrar::getFeatured('can_register', 'user'); |
| | | if (!empty($canRegister)) { |
| | | foreach ($canRegister as $role) { |
| | | $registrar = Registrar::getInstance($role); |
| | | $config = $registrar->getConfig('register'); |
| | | $icon = $registrar->getIcon('user'); |
| | | $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) { |
| | | $select[$role] = '<span class="label">'.$icon.$registrar->getSingular().'</span><span class="text">'.$config['description']??Site::login()->getDescription('register')??''.'</span>'; |
| | | if (!empty($config['fields'])){ |
| | | foreach ($config['fields'] as $field) { |
| | | $field['condition'] = [ |
| | | 'field' => 'user_select', |
| | | 'value' => $slug, |
| | | 'value' => $role, |
| | | 'operator' => '==' |
| | | ]; |
| | | $fields[] = $field; |
| | |
| | | |
| | | protected function setupFields():void |
| | | { |
| | | $this->fields = $this->getFieldsForAction($this->action); |
| | | } |
| | | |
| | | protected function getFieldsForAction(string $action):array |
| | | { |
| | | $fields = []; |
| | | switch($this->action) { |
| | | switch($action) { |
| | | case 'register': |
| | | $fields = $this->getRegistrationFormFields(); |
| | | break; |
| | |
| | | break; |
| | | |
| | | } |
| | | $this->fields = $fields; |
| | | return $fields; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Ensure login page exists |
| | | */ |
| | |
| | | } |
| | | } |
| | | } |
| | | public function loginUrl(string $login_url, string $redirect, bool $force_reauth):string |
| | | 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' ); |
| | |
| | | |
| | | protected function initMagicLinkSupport(): void |
| | | { |
| | | if (!Features::forSite()->has('magicLink')) { |
| | | if (!Site::has('magicLink')) { |
| | | return; |
| | | } |
| | | } |
| | |
| | | if (!$this->isLoginPage()) { |
| | | return $template; |
| | | } |
| | | global $_GET; |
| | | if (is_user_logged_in() && (!array_key_exists('action', $_GET) || $_GET['action']!=='logout')) { |
| | | wp_redirect(get_home_url(null, '/dash')); |
| | | exit; |
| | | } |
| | | $this->setup(); |
| | | $page = $this->cache->remember( |
| | | $this->getAction(), |
| | |
| | | .login main .login-box { |
| | | --gap: .75rem; |
| | | padding: 1rem; |
| | | background-color:rgba(var(--base-rgb),var(--op-6)); |
| | | border-radius: var(--outerRadius); |
| | | background-color: var(--overlay-heavy); |
| | | box-shadow: var(--shadow-right), var(--shadow-down); |
| | | margin: 15vh auto 0!important; |
| | | } |
| | |
| | | .login main .navigation, |
| | | .login main .login-box { |
| | | max-width: 60vw!important; |
| | | margin: 0 2rem 0 auto!important; |
| | | padding-right: 4rem!important; |
| | | margin: 0 0 0 auto!important; |
| | | } |
| | | .login main .login-box { |
| | | padding: 2rem; |
| | |
| | | { |
| | | $form = $this->action.'form'; |
| | | ?> |
| | | <section class="login-box col btw"> |
| | | <section class="login-box col y-btw"> |
| | | <h1><?=$this->labels['title']?></h1> |
| | | <?= $this->labels['description'] ?> |
| | | |
| | | <?= $this->renderLoginForm($this->action); ?> |
| | | |
| | | <form name="<?=$form?>" method="post" data-action="jvb_<?=$this->action?>"> |
| | | <?= jvbFormStatus() ?> |
| | | <?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) ?>"> |
| | | <?= ($this->action === 'magic') ? '<input type="hidden" name="type" value="login">' : '' ?> |
| | | <?php |
| | | do_action('jvb_add_token_inputs', $this->action); |
| | | |
| | | foreach ($this->fields as $name => $config) { |
| | | echo Form::render($name, '', $config); |
| | | } |
| | | |
| | | $this->maybeTurnstile(); |
| | | ?> |
| | | <div class="row btw nowrap"> |
| | | <button type="submit" class="button button-primary button-large"><?=$this->labels['submit']?></button> |
| | | <?php $this->maybeMagicLink(); ?> |
| | | </div> |
| | | </form> |
| | | |
| | | <?php |
| | | if (is_array($this->labels['extra'])) { |
| | |
| | | } |
| | | ?> |
| | | |
| | | <div class="options row btw"> |
| | | <div class="options row x-btw"> |
| | | <?php |
| | | switch ($this->action) { |
| | | case 'login': ?> |
| | |
| | | |
| | | </div> |
| | | </section> |
| | | <div class="navigation row btw"> |
| | | <div class="navigation row x-btw"> |
| | | <a href="<?= get_home_url() ?>">Home</a> |
| | | <?php |
| | | $privacy = get_privacy_policy_url(); |
| | |
| | | </div> |
| | | <?php |
| | | } |
| | | public function renderLoginForm(string $action = 'login', string $redirect = '', string $title = ''):string |
| | | { |
| | | ob_start(); |
| | | do_action('jvb_add_token_inputs', $this->action); |
| | | $additionalInputs = ob_get_clean(); |
| | | |
| | | $fields = ''; |
| | | $theFields = $this->getFieldsForAction($action); |
| | | foreach ($theFields as $name => $config) { |
| | | $fields .= Form::render($name, '', $config); |
| | | } |
| | | |
| | | ob_start(); |
| | | $this->maybeTurnstile(); |
| | | $turnstile = ob_get_clean(); |
| | | |
| | | ob_start(); |
| | | $this->maybeMagicLink(); |
| | | $magicLink = ob_get_clean(); |
| | | |
| | | $redirect = !empty($redirect) ? $redirect : esc_attr($_GET['redirect_to'] ?? ''); |
| | | |
| | | return sprintf( |
| | | '<form name="%sform" method="post" data-action="jvb_%s"> |
| | | %s%s%s |
| | | <input type="hidden" name="action" value="jvb_%s"> |
| | | <input type="hidden" name="redirect_to" value="%s"> |
| | | <input type="hidden" name="request_id" value="%s"> |
| | | %s |
| | | %s |
| | | %s |
| | | %s |
| | | <div class="row x-btw nowrap"> |
| | | <button type="submit" class="button button-primary button-large">%s</button> |
| | | %s |
| | | </div> |
| | | </form>', |
| | | $action, |
| | | $action, |
| | | jvbFormStatus(), |
| | | $title, |
| | | wp_nonce_field('jvb_'.$action), |
| | | $action, |
| | | $redirect, |
| | | wp_generate_password(16, false), |
| | | ($action === 'magic') ? '<input type="hidden" name="type" value="login">' : '', |
| | | $additionalInputs, |
| | | $fields, |
| | | $turnstile, |
| | | $this->labels['submit'], |
| | | $magicLink |
| | | ); |
| | | } |
| | | protected function renderHeader():void |
| | | { |
| | | ?> |
| | |
| | | <?php |
| | | $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"> |
| | | echo '<label title="'.$title.'" id="theme-switch" class="switch" for="theme-switcher"> |
| | | <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']). |
| | |
| | | |
| | | protected function maybeTurnstile(): void |
| | | { |
| | | if (!Features::hasIntegration('cloudflare')) { |
| | | if (!Site::hasIntegration('cloudflare')) { |
| | | return; |
| | | } |
| | | JVB()->connect('cloudflare')->renderTurnstile(); |
| | |
| | | |
| | | protected function maybeTurnstileScripts(): void |
| | | { |
| | | if (!Features::hasIntegration('cloudflare')) { |
| | | if (!Site::hasIntegration('cloudflare')) { |
| | | return; |
| | | } |
| | | JVB()->connect('cloudflare')->enqueueTurnstileScripts(); |
| | |
| | | |
| | | protected function verifyTurnstile(): bool |
| | | { |
| | | if (!Features::hasIntegration('cloudflare')) { |
| | | if (!Site::hasIntegration('cloudflare')) { |
| | | return true; // Not enabled, pass verification |
| | | } |
| | | |
| | |
| | | { |
| | | 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.)'], |
| | | ]; |
| | | return Site::login()->getLabels('register'); |
| | | 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'], |
| | | ]; |
| | | return Site::login()->getLabels('lostPassword'); |
| | | case 'resetpass': |
| | | case 'rp': |
| | | 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', |
| | | ]; |
| | | return Site::login()->getLabels('resetPassword'); |
| | | case 'logout': |
| | | return [ |
| | | 'title' => JVB_LOGIN['logout']['title'] ?? 'Logged Out!', |
| | | 'description' => JVB_LOGIN['logout']['description'] ?? [], |
| | | 'extra' => JVB_LOGIN['logout']['extra'] ?? [], |
| | | 'footer' => JVB_LOGIN['logout']['footer'] ?? '', |
| | | 'submit' => JVB_LOGIN['logout']['submit'] ?? '', |
| | | ]; |
| | | return Site::login()->getLabels('logout'); |
| | | case 'magic': |
| | | return [ |
| | | 'title' => JVB_LOGIN['magic']['title'] ?? 'Log in with Magic Link', |
| | | 'description' => JVB_LOGIN['magic']['description'] ?? ['Enter your email.','You\'ll get an email with a magic link.','Click it, and you\'re logged in!'], |
| | | 'extra' => JVB_LOGIN['magic']['extra'] ?? [], |
| | | 'footer' => JVB_LOGIN['magic']['footer'] ?? '', |
| | | 'submit' => JVB_LOGIN['magic']['submit'] ?? jvbIcon('magic-wand').'Send Magic Link', |
| | | |
| | | ]; |
| | | return Site::login()->getLabels('magic'); |
| | | 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', |
| | | ]; |
| | | return Site::login()->getLabels('login'); |
| | | } |
| | | } |
| | | |
| | |
| | | $action = $this->getAction(); |
| | | |
| | | $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : ''; |
| | | $has_turnstile = Features::hasIntegration('cloudflare'); |
| | | $has_turnstile = Site::hasIntegration('cloudflare'); |
| | | |
| | | ob_start(); |
| | | |
| | |
| | | if (!form || !window.jvbForm) return; |
| | | |
| | | window.jvbForm.registerForm(form, { |
| | | autosave: false, |
| | | endpoint: '<?= $action ?>', |
| | | formStatus: false, |
| | | showStatus: false, |
| | | cache: false, |
| | | }); |
| | | |
| | |
| | | if (result.redirect) { |
| | | setTimeout(() => { |
| | | window.location.href = result.redirect; |
| | | }, 100); |
| | | }, 20); |
| | | } |
| | | }); |
| | | } else if (result.redirect) { |
| | | setTimeout(() => { |
| | | window.location.href = result.redirect; |
| | | }, 100); |
| | | }, 20); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | |
| | | { |
| | | |
| | | } |
| | | public function setAction(string $action = 'login'):void |
| | | { |
| | | $this->action = $action; |
| | | $this->setup(); |
| | | } |
| | | } |
| | | |
| | | // Initialize the login manager |