124 lines
4.9 KiB
JavaScript
124 lines
4.9 KiB
JavaScript
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;
|