1/** 2 * Login Page Module 3 * Handles login form interactions and authentication. 4 * Supports two modes: 5 * - Root domain (smrtbld.ai): 3-field login with company code 6 * - Tenant subdomain (demo.smrtbld.ai): 2-field login, company auto-detected 7 */ 8 9import { auth } from '../auth.js'; 10import { setLoading } from '../utils/dom.js'; 11import { getTenantPrefix } from '../utils/tenant.js'; 12import { flagFieldError } from '../utils/form-validation.js'; 13 14// ======================================== 15// MODULE STATE 16// ======================================== 17 18let abortController = null; 19 20// ======================================== 21// PUBLIC API 22// ======================================== 23 24export function initLoginPage() { 25 abortController = new AbortController(); 26 const signal = abortController.signal; 27 28 // Cache DOM elements 29 const form = document.getElementById('loginForm'); 30 const companyCodeGroup = document.getElementById('companyCodeGroup'); 31 const companyCodeInput = document.getElementById('companyCode'); 32 const emailInput = document.getElementById('email'); 33 const passwordInput = document.getElementById('password'); 34 const togglePassword = document.getElementById('togglePassword'); 35 const eyeIcon = document.getElementById('eyeIcon'); 36 const eyeOffIcon = document.getElementById('eyeOffIcon'); 37 const errorMessage = document.getElementById('errorMessage'); 38 const errorText = document.getElementById('errorText'); 39 const loginButton = document.getElementById('loginButton'); 40 const tenantBanner = document.getElementById('tenantBanner'); 41 const tenantCompanyName = document.getElementById('tenantCompanyName'); 42 43 // Subdomain detection 44 const tenantPrefix = getTenantPrefix(); 45 let resolvedTenant = tenantPrefix; 46 47 if (tenantPrefix) { 48 // Hide company code field, show tenant banner 49 companyCodeGroup?.classList.add('hidden'); 50 tenantBanner?.classList.remove('hidden'); 51 if (tenantCompanyName) tenantCompanyName.textContent = 'Verifying...'; 52 53 // Move focus to email since company code is hidden 54 emailInput?.focus(); 55 56 // Validate the subdomain and show company name 57 auth.validateSubdomain(tenantPrefix).then(result => { 58 if (signal.aborted) return; 59 if ((result?.Valid || result?.valid) && (result?.CompanyName || result?.companyName)) { 60 if (tenantCompanyName) tenantCompanyName.textContent = result.CompanyName || result.companyName; 61 } else { 62 // Invalid subdomain â fall back to full login form 63 fallbackToFullForm('Company not found. Please enter your company code.'); 64 } 65 }).catch(() => { 66 if (signal.aborted) return; 67 // API error â degrade gracefully, show company code field 68 fallbackToFullForm(null); 69 }); 70 } else { 71 // Root domain mode â normal 3-field login 72 companyCodeInput?.setAttribute('required', ''); 73 } 74 75 /** Fall back from subdomain mode to full 3-field form */ 76 function fallbackToFullForm(errorMsg) { 77 tenantBanner?.classList.add('hidden'); 78 companyCodeGroup?.classList.remove('hidden'); 79 companyCodeInput?.setAttribute('required', ''); 80 companyCodeInput?.focus(); 81 resolvedTenant = null; 82 if (errorMsg) showError(errorMsg); 83 } 84 85 // Toggle password visibility 86 togglePassword?.addEventListener('click', () => { 87 const isPassword = passwordInput.type === 'password'; 88 passwordInput.type = isPassword ? 'text' : 'password'; 89 eyeIcon?.classList.toggle('hidden', !isPassword); 90 eyeOffIcon?.classList.toggle('hidden', isPassword); 91 }, { signal }); 92 93 // Form submission 94 form?.addEventListener('submit', async (e) => { 95 e.preventDefault(); 96 hideError(); 97 98 // Use detected tenant OR manual company code 99 const companyCode = resolvedTenant || companyCodeInput?.value.trim(); 100 const email = emailInput?.value.trim(); 101 const password = passwordInput?.value;
101 // deliberately untrimmed â a password of spaces is legal 102 103 if (!companyCode || !email || !password) { 104 // Flag what is ACTUALLY missing (sibling: portal/portal-enter.js password login). The 105 // company-code field is conditional: in subdomain mode resolvedTenant supplies it and 106 // #companyCodeGroup is hidden, so it is a target only while that group is visible. 107 // Inline text comes from each field's [data-error-for] node in login.html; the message 108 // here is the summary toast only (form-validation.js flagFieldError contract). 109 const companyCodeVisible = !companyCodeGroup?.classList.contains('hidden'); 110 const missing = [ 111 !companyCode && companyCodeVisible && companyCodeInput, 112 !email && emailInput, 113 !password && passwordInput, 114 ].filter(Boolean); 115 const names = missing.map(el => el === companyCodeInput ? 'company code' : el === emailInput ? 'email' : 'password'); 116 const message = 'Enter your ' + (names.length > 2 117 ? names.slice(0, -1).join(', ') + ' and ' + names.at(-1) 118 : names.join(' and ')); 119 if (missing.length) flagFieldError(form, missing, message, { clearOn: 'input' }); 120 else fallbackToFullForm('Please enter your company code'); // no visible offender (resolved tenant lost): re-show the field, never a silent dead-end 121 return; 122 } 123 124 loginButton.disabled = true; 125 setLoading(loginButton, true, 'Signing In...'); 126 127 try { 128 await auth.login(companyCode, email, password); 129 page('/'); 130 } catch (error) { 131 if (error.name === 'AbortError') return; 132 133 showError(error.message || 'Login failed. Please try again.'); 134 setLoading(loginButton, false); 135 } 136 }, { signal }); 137 138 function showError(message) { 139 if (errorText) errorText.textContent = message; 140 errorMessage?.classList.remove('hidden'); 141 } 142 143 function hideError() { 144 errorMessage?.classList.add('hidden'); 145 } 146} 147 148/** 149 * Cleanup function called on page transition. 150 */ 151export function destroyLoginPage() { 152 if (abortController) { 153 abortController.abort(); 154 abortController = null; 155 } 156}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.