// 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 || {}; // для GET используем cookie (без CORS preflight), для мутаций — Authorization const isGet = !opts.method || opts.method === 'GET' || opts.method === 'HEAD'; if (Store.access && !opts.headers.Authorization && !isGet) { opts.headers.Authorization = `Bearer ${Store.access}`; } if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(opts.body); } let r = await fetch(API_BASE + path, { ...opts, credentials: 'include' }); if (r.status === 401 && Store.refresh) { // refresh const r2 = await fetch(API_BASE + '/api/v1/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Refresh: Store.refresh }), }); if (r2.ok) { const tokens = await r2.json(); Store.save(tokens); opts.headers.Authorization = `Bearer ${Store.access}`; r = await fetch(API_BASE + path, opts); } else { Store.clear(); } } 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; } // === 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 };