import axios from 'axios'; /** * The Doormile Express admin API client. * * One axios instance for `api.doormile.com/api/v1/admin/*`. Every request needs * a JWT Bearer token, so the token is attached here rather than at each call * site, and an expired one logs the console out the same way a manual sign-out * does — a half-authenticated shell that renders but 401s on every fetch is * worse than being sent back to the login page. * * Ported from the Express Console's `utils/doormileAxios.js`. The only change * is the environment seam: Vite exposes `import.meta.env`, not `process.env`. */ export const DOORMILE_TOKEN_KEY = 'doormileToken'; export const DOORMILE_USER_KEY = 'doormileUser'; /** * The rest of the signed-in identity, written by Login.jsx because parts of the * data layer read it straight from storage rather than through context. * * Ending a session has to clear these too. They are not the credential, but * `tenantid` is what decides whether a login is Doormile staff or scoped to one * client — so a stale one left behind by the previous operator is both their * identity sitting readable in the browser and a value the next session could * read before signing in. */ export const DOORMILE_SESSION_KEYS = ['authname', 'firstname', 'userid', 'roleid', 'tenantid']; /** * Doormile AI's saved thread and its archive (owned by AIPanel.jsx's * HISTORY_KEY / CONVERSATIONS_KEY). * * These are session data, not a preference: the bot answers about live orders, * so a thread routinely contains order numbers, customer names and phone * numbers. Production users are warehouse and dispatch staff, often on a shared * terminal — leaving the transcript behind hands the next operator the last * one's work. The panel width is a preference and stays. */ export const DOORMILE_ASSISTANT_KEYS = ['doormileBotHistory', 'doormileBotConversations']; /** Clears every key that makes up a Doormile session. */ export const clearStoredSession = () => { localStorage.removeItem(DOORMILE_TOKEN_KEY); localStorage.removeItem(DOORMILE_USER_KEY); [...DOORMILE_SESSION_KEYS, ...DOORMILE_ASSISTANT_KEYS].forEach((key) => localStorage.removeItem(key)); }; /** The backend, for every `/admin/*` call the console makes. */ export const DOORMILE_API_URL = 'https://api.doormile.com/api/v1'; /** * `VITE_DOORMILE_URL` exists to point a local build at a staging or mock * backend. It is never set in production: `.dockerignore` keeps every `.env` * file out of the image, so a container build always resolves to the real API. * * An override is announced loudly in development, because a console silently * talking to the wrong backend looks exactly like a console talking to the * right one with no data in it. */ export const DOORMILE_BASE_URL = import.meta.env.VITE_DOORMILE_URL || DOORMILE_API_URL; if (import.meta.env.DEV && DOORMILE_BASE_URL !== DOORMILE_API_URL) { console.warn( `[Doormile] API overridden to ${DOORMILE_BASE_URL} — the real backend is ${DOORMILE_API_URL}. ` + 'Unset VITE_DOORMILE_URL to use it.' ); } const doormileAxios = axios.create({ baseURL: DOORMILE_BASE_URL }); doormileAxios.interceptors.request.use((config) => { const token = localStorage.getItem(DOORMILE_TOKEN_KEY); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); doormileAxios.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401 && !window.location.href.includes('/login')) { clearStoredSession(); window.location.replace('/login'); } /* Reject with the server's own body — callers read `err.message` — but keep the HTTP status reachable as `err.httpStatus`. Without this the status is lost entirely and a 404, a 500 and a validation failure all collapse into the same generic failure, which is exactly the distinction the write paths need. Non-enumerable so nothing that spreads or stringifies the body starts carrying it around. */ const body = error.response?.data; if (body === undefined || body === null) return Promise.reject(error); const payload = typeof body === 'object' ? body : { message: String(body) }; return Promise.reject( Object.defineProperty(payload, 'httpStatus', { value: error.response?.status, enumerable: false, }) ); } ); /** Reads the signed-in admin back out of storage, or null when signed out. */ export const readStoredUser = () => { try { const raw = localStorage.getItem(DOORMILE_USER_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } }; export const readStoredToken = () => localStorage.getItem(DOORMILE_TOKEN_KEY); /** The message a failed call should show — server text first, then the throw. */ export const errorMessage = (err, fallback = 'Something went wrong') => err?.message || err?.response?.data?.message || fallback; export default doormileAxios;