PageSourceSearch

https://smrtbld.ai/js/pages/login.js

js smrtbld.ai collected 2026-10-05 15:07:13 UTC 6,438 bytes, 156 lines download raw bytes

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.