- vendor/leaflet.js (no more CDN dependency for JS)
- direct tile.openstreetmap.org (was {s}.tile)
- backend GET /api/v1/auth/consents (check if all accepted)
- frontend ConsentApi.status() before showing overlay
- only show overlay once
176 lines
5.3 KiB
JavaScript
176 lines
5.3 KiB
JavaScript
// 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 || {};
|
||
if (Store.access && !opts.headers.Authorization) {
|
||
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);
|
||
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 });
|
||
},
|
||
};
|
||
|
||
window.API = { api, Auth, ChatApi, ReviewApi, ConsentApi, Store }; |