buhapp-telegram/public/js/api.js
root cb05669c7c Fix auth: backend returns Access/Refresh (CamelCase), JS expected access/refresh
Store.save() now handles both cases. Refresh endpoint uses 'Refresh' key.
This was causing /me 401 right after register.
2026-08-20 18:36:41 +00:00

163 lines
5.0 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 || {};
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'); },
};
window.API = { api, Auth, ChatApi, ReviewApi, Store };