1/** 2 * FTCAuth â shared customer session for followthecamino.com. 3 * 4 * Owns the JWT contract that the site header (Impreza-child/js/custom.js) already 5 * uses â localStorage `jwtToken` / `jwtUser` + POST /api/login_check + GET 6 * /api/user_info â and exposes a small stable interface so the header, the 7 * AutoQuote widget and the chatbot stop duplicating it: 8 * 9 * FTCAuth.isLogged() -> boolean (has a token; not yet validated) 10 * FTCAuth.whoami({force}) -> Promise<user|null> (validates via user_info) 11 * FTCAuth.user() -> cached user|null (sync) 12 * FTCAuth.login(email, pass) -> Promise<user> (login_check + cache + emit) 13 * FTCAuth.logout() -> clears session + emit 14 * FTCAuth.refresh() -> re-validate + emit 15 * FTCAuth.onChange(cb)/offChange(cb) 16 * 17 * It also fires a DOM event `ftc:auth:changed` (detail.user) on login/logout/ 18 * refresh and on cross-tab storage changes, so consumers can react without 19 * polling. Pure helper `normalizeUser` is exported for tests. 20 */ 21(function () { 22 'use strict'; 23 24 var cfg = (typeof window !== 'undefined' && window.FTCAuthConfig) || {}; 25 var API_BASE = cfg.apiBase || 'https://api.followthecamino.com'; 26 /* Namespaced to the customer session. The old generic jwtToken/jwtUser were 27 also written by the staff back-office, so an employee token could sit in a 28 pilgrim's browser and be read back here as if it were the customer's. */ 29 var TOKEN_KEY = cfg.tokenKey || 'ftcCustomerToken'; 30 var USER_KEY = cfg.userKey || 'ftcCustomerUser'; 31 var DEFAULT_TOKEN_KEY = 'ftcCustomerToken'; 32 /* The pre-rename keys, still written by the site header and the chatbot. 33 Adopted on load rather than deleted: deleting them outright would log the 34 customer out of the header on every page view until those consumers move to 35 the new key. They ARE deleted when the session turns out to be staff. */ 36 var LEGACY_KEYS = cfg.legacyKeys || ['jwtToken', 'jwtUser']; 37 38 /* ...but only when this install uses the default key. A site that overrides 39 it is deliberately isolating its session â ftc.local points the widget at a 40 local API with a different JWT signing key and its own storage key. There, 41 the legacy `jwtToken` belongs to a DIFFERENT backend: adopting it would 42 import a token that cannot validate, and the failed validation would then 43 purge the legacy keys, signing the user out of the site header. */ 44 function adoptsLegacy(config) { 45 config = config || {}; 46 var key = config.tokenKey; 47 var legacy = config.legacyKeys || LEGACY_KEYS; 48 return key === DEFAULT_TOKEN_KEY && legacy.length > 0; 49 } 50 51 /* Only a customer session may drive the public site. Allowlisted rather than 52 denylisted, so a role we have never seen is treated as staff, not as a 53 customer. A staff account that also carries the client role is still staff. */ 54 var CUSTOMER_ROLE = 'ROLE_CLIENT_ROLE'; 55 var STAFF_ROLES = ['ROLE_ADMIN_ROLE', 'ROLE_SUPPLIER_ROLE']; 56 57 function isCustomer(user) { 58 if (!user || !Array.isArray(user.roles)) { return false; } 59 for (var i = 0; i < user.roles.length; i++) { 60 if (STAFF_ROLES.indexOf(user.roles[i]) !== -1) { return false; } 61 } 62 return user.roles.indexOf(CUSTOMER_ROLE) !== -1; 63 } 64 65 /** 66 * Map the /api/user_info payload to a stable shape. The endpoint returns 67 * `username` (the login email, no separate email field) and `contactPhone` 68 * ({area_code, number}); normalize both so consumers never touch raw keys. 69 */ 70 function normalizeUser(info) { 71 if (!info || !info.username) { return null; } 72 var phone = info.contactPhone || null; 73 return { 74 id: info.id || null, 75 email: info.username, 76 firstName: info.firstName || '', 77 name: info.name || '', 78 roles: info.roles || [], 79 phone: phone ? { areaCode: phone.area_code || '', number: phone.number || '' } : null, 80 raw: info 81 }; 82 } 83 84 /* ---- browser-only state below (guards keep this require-safe in node) ---- */ 85 86 function hasStorage() { 87 try { return typeof window !== 'undefined' && !!window.localStorage; } catch (e) { return false; } 88 } 89 function getToken() { return hasStorage() ? window.localStorage.getItem(TOKEN_KEY) : null; } 90 function setToken(token, firstName) { 91 if (!hasStorage()) { return; } 92 window.localStorage.setItem(TOKEN_KEY, token); 93 if (firstName != null) { window.localStorage.setItem(USER_KEY, firstName); } 94 } 95 function clearToken() { 96 if (!hasStorage()) { return; } 97 window.localStorage.removeItem(TOKEN_KEY); 98 window.localStorage.removeItem(USER_KEY); 99 } 100 /* Carry an existing session over to the namespaced key, so upgrading does not 101 sign the customer out. Only fills a gap â never overwrites a current token. */ 102 function adoptLegacyToken() { 103 if (!hasStorage() || getToken()) { return; } 104 if (!adoptsLegacy({ tokenKey: TOKEN_KEY, legacyKeys: LEGACY_KEYS })) { return; } 105 try { 106 var legacy = window.localStorage.getItem(LEGACY_KEYS[0]); 107 if (legacy) { 108 window.localStorage.setItem(TOKEN_KEY, legacy); 109 var name = LEGACY_KEYS[1] ? window.localStorage.getItem(LEGACY_KEYS[1]) : null; 110 if (name) { window.localStorage.setItem(USER_KEY, name); } 111 } 112 } catch (e) { /* quota / private mode */ } 113 } 114 /* A staff token is toxic on the public site, so it is removed from the legacy 115 keys too â otherwise it would simply be adopted again on the next load. */ 116 function purgeLegacyKeys() { 117 /* Same guard: never touch another backend's keys. */ 118 if (!hasStorage() || !adoptsLegacy({ tokenKey: TOKEN_KEY, legacyKeys: LEGACY_KEYS })) { return; } 119 for (var i = 0; i < LEGACY_KEYS.length; i++) {
120 try { window.localStorage.removeItem(LEGACY_KEYS[i]); } catch (e) { /* quota/private mode */ } 121 } 122 } 123 124 var cachedUser = null; 125 var listeners = []; 126 127 function emit() { 128 var u = cachedUser; 129 for (var i = 0; i < listeners.length; i++) { 130 try { listeners[i](u); } catch (e) { /* a bad subscriber must not break the rest */ } 131 } 132 if (typeof document !== 'undefined' && document.dispatchEvent && typeof CustomEvent === 'function') { 133 try { document.dispatchEvent(new CustomEvent('ftc:auth:changed', { detail: { user: u } })); } catch (e) {} 134 } 135 } 136 137 function fetchUserInfo(token) { 138 return fetch(API_BASE + '/api/user_info', { headers: { Authorization: 'Bearer ' + token } }) 139 .then(function (r) { if (!r.ok) { throw new Error('user_info ' + r.status); } return r.json(); }); 140 } 141 142 function whoami(opts) { 143 opts = opts || {}; 144 var token = getToken(); 145 if (!token) { cachedUser = null; return Promise.resolve(null); } 146 if (cachedUser && !opts.force) { return Promise.resolve(cachedUser); } 147 return fetchUserInfo(token).then(function (info) { 148 var candidate = normalizeUser(info); 149 /* A valid token that is not a customer's (an employee signed in on this 150 browser) is discarded rather than used: it would otherwise put a staff 151 identity on a pilgrim's quote or booking. */ 152 if (!isCustomer(candidate)) { 153 clearToken(); 154 purgeLegacyKeys(); 155 cachedUser = null; 156 return null; 157 } 158 cachedUser = candidate; 159 if (hasStorage()) { window.localStorage.setItem(USER_KEY, cachedUser.firstName); } 160 return cachedUser; 161 }).catch(function () { 162 // Invalid/expired token â clear it so the UI falls back to logged-out. 163 clearToken(); 164 cachedUser = null; 165 return null; 166 }); 167 } 168 169 function login(email, password) { 170 return fetch(API_BASE + '/api/login_check', { 171 method: 'POST', 172 headers: { 'Content-Type': 'application/json' }, 173 body: JSON.stringify({ username: email, password: password }) 174 }).then(function (r) { 175 if (!r.ok) { throw new Error('login ' + r.status); } 176 return r.json(); 177 }).then(function (data) { 178 setToken(data.token, null); 179 return whoami({ force: true }); 180 }).then(function (u) { 181 emit(); 182 /* whoami already dropped a non-customer token; surface it as a failed 183 login so the caller shows an error instead of a half-signed-in UI. */ 184 if (!u) { throw new Error('not a customer account'); } 185 return u; 186 }); 187 } 188 189 function logout() { clearToken(); cachedUser = null; emit(); } 190 function refresh() { return whoami({ force: true }).then(function (u) { emit(); return u; }); } 191 function isLogged() { return !!getToken(); } 192 function user() { return cachedUser; } 193 function onChange(cb) { if (typeof cb === 'function') { listeners.push(cb); } } 194 function offChange(cb) { listeners = listeners.filter(function (f) { return f !== cb; }); } 195 196 // Cross-tab: when another tab logs in/out, re-validate and notify here. 197 if (typeof window !== 'undefined' && window.addEventListener) { 198 window.addEventListener('storage', function (e) { 199 if (e.key === TOKEN_KEY) { cachedUser = null; whoami({ force: true }).then(emit); } 200 }); 201 } 202 203 adoptLegacyToken(); 204 205 var FTCAuth = { 206 normalizeUser: normalizeUser, 207 isCustomer: isCustomer, 208 adoptsLegacy: adoptsLegacy, 209 token: getToken, 210 isLogged: isLogged, 211 user: user, 212 whoami: whoami, 213 login: login, 214 logout: logout, 215 refresh: refresh, 216 onChange: onChange, 217 offChange: offChange, 218 config: { 219 apiBase: API_BASE, tokenKey: TOKEN_KEY, userKey: USER_KEY, 220 legacyKeys: LEGACY_KEYS 221 } 222 }; 223 224 if (typeof module !== 'undefined' && module.exports) { module.exports = FTCAuth; } 225 if (typeof window !== 'undefined') { window.FTCAuth = FTCAuth; } 226 227 return FTCAuth; 228})();
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.