import ky, { type KyInstance } from "ky"; import { pushEntry, shortPath } from "@/lib/reqlog"; import { useAuth } from "@/store/auth"; export const API_BASE = import.meta.env.VITE_API_BASE ?? "https://crm.pptaxi.ru/api"; /** Засечка времени на конкретный запрос. WeakMap — чтобы не держать Request'ы. */ const started = new WeakMap(); /** * Заголовок, которым бэкенд отдаёт ПРОДЛЁННЫЙ токен водителя (см. driver_auth.py). * * Токен живёт 30 дней, и без продления они считались бы от входа: раз в месяц * водителя выкидывало бы на экран входа, а «войти» для него означает сходить в * бота за кодом. Начиная с пятого дня жизни токена бэкенд кладёт в ответ свежий, * мы его молча подменяем — и 30 дней начинают считаться от последнего захода. */ const RENEWED_TOKEN_HEADER = "X-Driver-Token"; /** Подменить токен на продлённый, если ответ его принёс. */ function adoptRenewedToken(request: Request, response: Response): void { const fresh = response.headers.get(RENEWED_TOKEN_HEADER); if (!fresh) return; const auth = useAuth.getState(); // Сеанс мог смениться, пока запрос был в пути: водитель вышел, сотрудник начал // просмотр от чужого лица. Подменяем только тот токен, с которым сами ушли, — // иначе продление чужого ответа вернёт закрытый сеанс к жизни. if (auth.impersonated || auth.driverId == null) return; if (request.headers.get("Authorization") !== `Bearer ${auth.token}`) return; auth.setSession(fresh, auth.driverId); } export const api: KyInstance = ky.create({ prefixUrl: API_BASE, timeout: 30_000, retry: { limit: 2, methods: ["get"] }, hooks: { beforeRequest: [ (request) => { started.set(request, performance.now()); const token = useAuth.getState().token; if (token) request.headers.set("Authorization", `Bearer ${token}`); }, ], // В журнал попадают ОТВЕТИВШИЕ запросы, включая 4xx/5xx. Запрос, оборвавшийся // без ответа (таймаут, пропавшая сеть), ky сюда не приносит — на экране // отладки об этом сказано прямо, чтобы пустая строка не читалась как «дошло». afterResponse: [ (request, _options, response) => { pushEntry({ method: request.method, path: shortPath(request.url), status: response.status, ms: Math.round(performance.now() - (started.get(request) ?? performance.now())), }); adoptRenewedToken(request, response); }, ], beforeError: [ (error) => { if (error.response?.status === 401) useAuth.getState().clear(); return error; }, ], }, });