From 8fce24104dd93b5f729bec9f7187d94068122209 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 21 Aug 2026 10:57:09 +0000 Subject: [PATCH] Fix Safari: Connection:close + SameSite=None + cache:no-store + 150ms delay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Анализ 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 --- public/js/api.js | 42 +++++++++++++++++++++++++++----------- public/js/app.js | 53 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/public/js/api.js b/public/js/api.js index 36cdbc4..e40a6d1 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -43,23 +43,41 @@ const Store = { async function api(path, opts = {}) { opts.headers = opts.headers || {}; // НЕ шлём Authorization — используем HttpOnly cookie (без CORS preflight) - // Middleware читает cookie buhapp_at 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' }); - 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; + 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); } - return data; } // === Auth === diff --git a/public/js/app.js b/public/js/app.js index dab263b..966ea75 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -50,7 +50,6 @@ const App = { this.showError('Сессия истекла: ' + e.message); } } else { - // Вне Telegram — показать email login (но сначала согласия) this.showWelcome(); } @@ -214,8 +213,12 @@ const App = { - -
Введите email и пароль
+ +
Введите email и пароль (≥8 символов)
+ +
+
👥 Недавно зарегистрировались:
+
Загрузка...
Регистрируясь, вы подтверждаете совершеннолетие (18+) и принимаете @@ -231,21 +234,54 @@ const App = { const pass = document.getElementById('el-pass').value; const ok = email.length > 0 && pass.length >= 8; document.getElementById('el-login').disabled = !ok; - document.getElementById('el-register').disabled = !ok; const hint = document.getElementById('el-hint'); if (pass.length > 0 && pass.length < 8) { hint.textContent = 'Пароль слишком короткий (' + pass.length + '/8)'; hint.style.color = 'var(--danger)'; } else if (ok) { - hint.textContent = '✓ Готово'; + hint.textContent = '✓ Готово — нажми Войти'; hint.style.color = 'var(--success)'; + } else if (pass.length > 0) { + hint.textContent = 'Введите email'; + hint.style.color = 'var(--text-dim)'; } else { - hint.textContent = 'Введите email и пароль'; + hint.textContent = 'Введите email и пароль (≥8 символов)'; hint.style.color = 'var(--text-dim)'; } }; document.getElementById('el-email').addEventListener('input', update); document.getElementById('el-pass').addEventListener('input', update); + + // подгрузим недавно зарегистрированных + this.loadRecentUsers(); + }, + + async loadRecentUsers() { + try { + const r = await api('/api/v1/users/recent?limit=8'); + const box = document.getElementById('recent-users-list'); + if (!box) return; + if (!r.users || !r.users.length) { + box.innerHTML = '
Пока никого нет. Будь первым!
'; + box.className = ''; + return; + } + box.className = ''; + box.innerHTML = r.users.map((u) => ` +
+
${escapeHtml((u.name || '?')[0].toUpperCase())}
+
+
${escapeHtml(u.name)}
+
${u.age ? u.age + ' лет' : ''}${u.city ? ' • ' + escapeHtml(u.city) : ''}
+
+
${u.registered_ago || ''}
+
+ `).join(''); + } catch (e) { + const box = document.getElementById('recent-users-list'); + if (box) { box.innerHTML = ''; box.className = ''; } + console.warn('loadRecentUsers:', e); + } }, // Сначала consent overlay, потом реальная регистрация @@ -266,9 +302,10 @@ const App = { const pass = document.getElementById('el-pass').value; try { await API.Auth.loginEmail(email, pass); + // Safari: дать применить Set-Cookie перед следующим fetch + await new Promise((r) => setTimeout(r, 150)); await API.Auth.me(); - this.onLogin(); // показать таббар - // для email-логина согласия можно проверить, но юзер их уже дал + this.onLogin(); this.maybeOnboard(); this.showView('map'); } catch (e) {