chat: same-origin /chat/api proxy вместо прямого CORS к Mattermost

Браузер больше не ходит на chat.* с portal.* — REST идёт через Django с PAT;
WS backoff без бесконечных 403; utilities.js ждёт DOMContentLoaded.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 12:53:04 +03:00
parent fe463c11af
commit 4c2984a81e
7 changed files with 228 additions and 15 deletions
+56 -9
View File
@@ -131,6 +131,7 @@ function chatInit() {
chatShowArchived = false;
chatPrefDirectShow = {};
chatPrefGroupShow = {};
chatWsFailures = 0;
chatLoadTeams().then(chatLoadPreferences).then(chatLoadChannels).then(chatOpenPendingDm);
chatConnectSocket();
@@ -172,17 +173,45 @@ function chatOpenDmWithUser(userId) {
}).catch(function (e) { console.error(e); });
}
function chatCsrfToken() {
var m = document.cookie.match(/(?:^|; )csrftoken=([^;]*)/);
return m ? decodeURIComponent(m[1]) : '';
}
function chatApiFetch(path, options) {
options = options || {};
options.headers = Object.assign({'Authorization': 'Bearer ' + chatConfig.token}, options.headers || {});
var headers = Object.assign({
'Authorization': 'Bearer ' + chatConfig.token,
'X-CSRFToken': chatCsrfToken(),
}, options.headers || {});
// FormData: не фиксируем Content-Type — браузер сам поставит boundary
if (typeof FormData !== 'undefined' && options.body instanceof FormData) {
delete headers['Content-Type'];
}
options.headers = headers;
return fetch(chatConfig.apiBase + path, options).then(function (res) {
if (!res.ok) throw new Error('Mattermost API ' + path + ' -> ' + res.status);
return res.json();
if (!res.ok) {
var err = new Error('Mattermost API ' + path + ' -> ' + res.status);
err.status = res.status;
throw err;
}
var ct = res.headers.get('content-type') || '';
if (ct.indexOf('application/json') >= 0 || ct.indexOf('+json') >= 0) {
return res.json();
}
return res.text().then(function (t) {
try { return JSON.parse(t); } catch (e) { return t; }
});
});
}
function chatApiFetchBlob(path) {
return fetch(chatConfig.apiBase + path, {headers: {'Authorization': 'Bearer ' + chatConfig.token}}).then(function (res) {
return fetch(chatConfig.apiBase + path, {
headers: {
'Authorization': 'Bearer ' + chatConfig.token,
'X-CSRFToken': chatCsrfToken(),
},
}).then(function (res) {
if (!res.ok) throw new Error('Mattermost API ' + path + ' -> ' + res.status);
return res.blob();
});
@@ -306,8 +335,9 @@ function chatLoadChannels() {
chatLoadLastActivity(); // уточнюємо порядок за реальним часом останніх постів
}).catch(function (e) {
console.error(e);
var detail = (e && e.status) ? (' (' + e.status + ')') : '';
var msg = '<div style="padding:1rem; text-align:center; font-size:0.8125rem; color:var(--color-destructive);">'
+ gettext('Не удалось загрузить каналы') + '</div>';
+ gettext('Не удалось загрузить каналы') + detail + '</div>';
var el = document.getElementById('chat-channel-list');
if (el) el.innerHTML = msg;
var dm = document.getElementById('chat-dm-list');
@@ -1169,12 +1199,27 @@ function chatStartNewDm() {
}).catch(function (e) { console.error(e); });
}
var chatWsFailures = 0;
var chatWsMaxBackoff = 60000;
function chatConnectSocket() {
if (!chatConfig) return;
var socket = new WebSocket(chatConfig.wsUrl);
if (!chatConfig || !chatConfig.wsUrl) return;
// Live WS идёт на public_url Mattermost (не через Django). Без AllowCorsFrom /
// разрешения Origin на chat.* будет 403 — не долбим реконнект бесконечно.
if (chatWsFailures >= 8) return;
var socket;
try {
socket = new WebSocket(chatConfig.wsUrl);
} catch (e) {
console.warn('chat websocket create failed', e);
chatWsFailures += 1;
return;
}
chatSocket = socket;
socket.onopen = function () {
chatWsFailures = 0;
socket.send(JSON.stringify({seq: 1, action: 'authentication_challenge', data: {token: chatConfig.token}}));
};
@@ -1187,11 +1232,13 @@ function chatConnectSocket() {
socket.onclose = function () {
if (!document.getElementById('chat-app')) return;
chatReconnectTimer = setTimeout(chatConnectSocket, 3000);
chatWsFailures += 1;
var delay = Math.min(chatWsMaxBackoff, 3000 * Math.pow(2, Math.min(chatWsFailures - 1, 4)));
chatReconnectTimer = setTimeout(chatConnectSocket, delay);
};
socket.onerror = function () {
socket.close();
try { socket.close(); } catch (e) { /* ignore */ }
};
}