PageSourceSearch

https://followthecamino.com/wp-content/plugins/ftc-auth/assets/ftc-auth.js?ver=0.2.0

js followthecamino.com collected 2026-10-02 05:21:51 UTC 9,641 bytes, 228 lines download raw bytes

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.