buhapp-telegram/public/js/api.js
root 8fce24104d Fix Safari: Connection:close + SameSite=None + cache:no-store + 150ms delay
Анализ Grok: Safari зависает на ответе fetch после POST с Set-Cookie из-за keep-alive/HTTP2 reuse.
- backend Login/Register: Connection: close, Cache-Control: no-store
- backend cookie: SameSite=None (cross-subdomain в Safari)
- frontend api(): cache:'no-store', mode:'cors', timeout 20s
- frontend emailLogin: 150ms пауза после login перед следующим fetch
2026-08-21 10:57:09 +00:00

187 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// API клиент
const API_BASE = window.API_BASE || 'https://api.buhapp.mygoodservice.ru';
const Store = {
access: null,
refresh: null,
user: null,
save(tokens) {
if (!tokens) return;
// бэкенд отдаёт {Access, Refresh} — поддержим оба варианта
this.access = tokens.Access || tokens.access;
this.refresh = tokens.Refresh || tokens.refresh;
try {
localStorage.setItem('buhapp', JSON.stringify({
access: this.access,
refresh: this.refresh,
user: this.user,
}));
} catch {}
},
load() {
try {
const s = JSON.parse(localStorage.getItem('buhapp') || 'null');
if (s) {
this.access = s.access || s.Access;
this.refresh = s.refresh || s.Refresh;
this.user = s.user;
}
} catch {}
return this.access;
},
clear() {
this.access = null;
this.refresh = null;
this.user = null;
try { localStorage.removeItem('buhapp'); } catch {}
},
};
async function api(path, opts = {}) {
opts.headers = opts.headers || {};
// НЕ шлём Authorization — используем HttpOnly cookie (без CORS preflight)
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(opts.body);
}
opts.headers['Accept'] = opts.headers['Accept'] || 'application/json';
// timeout 20s — Safari иногда зависает на ответе после keep-alive
const controller = new AbortController();
const tid = setTimeout(() => controller.abort(), 20000);
try {
let r = await fetch(API_BASE + path, {
...opts,
credentials: 'include',
cache: 'no-store',
mode: 'cors',
redirect: 'follow',
signal: controller.signal,
});
const text = await r.text();
let data = null;
try { data = text ? JSON.parse(text) : null; } catch { data = text; }
if (!r.ok) {
const msg = (data && data.error) || `HTTP ${r.status}`;
const err = new Error(msg);
err.status = r.status;
err.data = data;
throw err;
}
return data;
} catch (e) {
if (e.name === 'AbortError') throw new Error('Сервер не отвечает (таймаут 20с)');
throw e;
} finally {
clearTimeout(tid);
}
}
// === Auth ===
const Auth = {
async loginWithTelegram(initData) {
const r = await api('/api/v1/auth/telegram', {
method: 'POST',
body: { init_data: initData },
});
Store.save(r.tokens);
Store.user = r.user;
return r;
},
async registerEmail(data) {
const r = await api('/api/v1/auth/register', { method: 'POST', body: data });
Store.save(r.tokens);
Store.user = r.user;
return r;
},
async loginEmail(email, password) {
const r = await api('/api/v1/auth/login', { method: 'POST', body: { email, password } });
Store.save(r.tokens);
Store.user = r.user;
return r;
},
async me() {
const r = await api('/api/v1/me');
Store.user = r.user;
return r;
},
async updateMe(p) {
return api('/api/v1/me', { method: 'PUT', body: p });
},
async getPrefs() { return api('/api/v1/me/prefs'); },
async updatePrefs(p) { return api('/api/v1/me/prefs', { method: 'PUT', body: p }); },
async updateLocation(lat, lng, visible) {
return api('/api/v1/me/location', { method: 'PUT', body: { lat, lng, visible } });
},
async setVisibility(v) { return api('/api/v1/me/visibility', { method: 'PUT', body: { visible: v } }); },
async searchNearby(lat, lng, radiusKm) {
return api('/api/v1/search/nearby?lat=' + lat + '&lng=' + lng + '&radius=' + radiusKm);
},
async getUser(id) { return api('/api/v1/users/' + id); },
};
// === Chat ===
const ChatApi = {
async ensureChat(otherId) { return api('/api/v1/chats', { method: 'POST', body: { other_id: otherId } }); },
async listChats() { return api('/api/v1/chats'); },
async listMessages(chatId) { return api('/api/v1/chats/' + chatId + '/messages'); },
async sendMessage(chatId, body, photoUrl) {
return api('/api/v1/chats/' + chatId + '/messages', { method: 'POST', body: { body, photo_url: photoUrl } });
},
async editMessage(msgId, body) {
return api('/api/v1/messages/' + msgId, { method: 'PUT', body: { body } });
},
async deleteMessage(msgId) {
return api('/api/v1/messages/' + msgId, { method: 'DELETE' });
},
async markRead(chatId) {
return api('/api/v1/chats/' + chatId + '/read', { method: 'PUT', body: {} });
},
async block(userId) { return api('/api/v1/blocks', { method: 'POST', body: { user_id: userId } }); },
async report(type, id, reason) { return api('/api/v1/reports', { method: 'POST', body: { target_type: type, target_id: id, reason } }); },
};
// === Reviews ===
const ReviewApi = {
async create(chatId, rating, body, anonymous) {
return api('/api/v1/reviews', { method: 'POST', body: { chat_id: chatId, rating, body, anonymous } });
},
async listForUser(userId) { return api('/api/v1/users/' + userId + '/reviews'); },
async stats(userId) { return api('/api/v1/users/' + userId + '/stats'); },
};
const ConsentApi = {
async status() {
try {
return await api('/api/v1/auth/consents');
} catch (e) {
return { all_accepted: false, accepted: {} };
}
},
async accept(payload) {
return api('/api/v1/auth/consents', { method: 'POST', body: payload });
},
};
const PlacesApi = {
async nearby(lat, lng, radius, limit = 30) {
try {
return await api(`/api/v1/places/nearby?lat=${lat}&lng=${lng}&radius=${radius || 5}&limit=${limit}`);
} catch (e) {
return { places: [], count: 0, error: e.message };
}
},
};
window.API = { api, Auth, ChatApi, ReviewApi, ConsentApi, PlacesApi, Store };