Бэкенд с пятого дня жизни токена кладёт продлённый в заголовок X-Driver-Token (crm2, modules/payments/driver_auth). Здесь — подмена: тридцать дней начинают считаться от последнего захода, а не от входа, и водитель перестаёт раз в месяц упираться в экран входа. Подменяем только тот токен, с которым сами ушли. Сеанс мог смениться, пока запрос был в пути — водитель вышел, сотрудник начал просмотр от чужого лица, — и продление чужого ответа вернуло бы закрытый сеанс к жизни. Просмотр от лица водителя не продлеваем: его восемь часов сознательные. README: приложение запущено для всех 22.08.2026, гейт по номерам снят. Раздел оставлен как инструкция на случай, если вход снова понадобится сузить. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
3.3 KiB
TypeScript
67 lines
3.3 KiB
TypeScript
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<Request, number>();
|
|
|
|
/**
|
|
* Заголовок, которым бэкенд отдаёт ПРОДЛЁННЫЙ токен водителя (см. 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;
|
|
},
|
|
],
|
|
},
|
|
});
|