Fix Safari: Connection:close + SameSite=None + cache:no-store + 150ms delay
Анализ 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
This commit is contained in:
parent
0fa3dcc573
commit
8fce24104d
@ -43,12 +43,24 @@ 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' });
|
||||
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; }
|
||||
@ -60,6 +72,12 @@ async function api(path, opts = {}) {
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') throw new Error('Сервер не отвечает (таймаут 20с)');
|
||||
throw e;
|
||||
} finally {
|
||||
clearTimeout(tid);
|
||||
}
|
||||
}
|
||||
|
||||
// === Auth ===
|
||||
|
||||
@ -50,7 +50,6 @@ const App = {
|
||||
this.showError('Сессия истекла: ' + e.message);
|
||||
}
|
||||
} else {
|
||||
// Вне Telegram — показать email login (но сначала согласия)
|
||||
this.showWelcome();
|
||||
}
|
||||
|
||||
@ -214,8 +213,12 @@ const App = {
|
||||
<label>Пароль (≥8 символов)</label>
|
||||
<input class="input" id="el-pass" type="password" placeholder="минимум 8 символов" autocomplete="current-password">
|
||||
<button class="btn" id="el-login" disabled>Войти</button>
|
||||
<button class="btn ghost" id="el-register" disabled>Регистрация</button>
|
||||
<div class="sub" id="el-hint" style="margin-top:8px;text-align:center;">Введите email и пароль</div>
|
||||
<button class="btn ghost" id="el-register">Регистрация</button>
|
||||
<div class="sub" id="el-hint" style="margin-top:8px;text-align:center;">Введите email и пароль (≥8 символов)</div>
|
||||
</div>
|
||||
<div class="card" id="recent-users-card">
|
||||
<div class="sub">👥 Недавно зарегистрировались:</div>
|
||||
<div id="recent-users-list" class="loader" style="min-height:60px;padding:8px 0;">Загрузка...</div>
|
||||
</div>
|
||||
<div class="card" style="font-size:12px;color:var(--text-dim);">
|
||||
Регистрируясь, вы подтверждаете совершеннолетие (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 = '<div class="empty" style="font-size:13px;">Пока никого нет. Будь первым!</div>';
|
||||
box.className = '';
|
||||
return;
|
||||
}
|
||||
box.className = '';
|
||||
box.innerHTML = r.users.map((u) => `
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--border);">
|
||||
<div class="avatar" style="width:32px;height:32px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;flex-shrink:0;">${escapeHtml((u.name || '?')[0].toUpperCase())}</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-size:14px;font-weight:500;">${escapeHtml(u.name)}</div>
|
||||
<div class="privacy-note" style="font-size:11px;">${u.age ? u.age + ' лет' : ''}${u.city ? ' • ' + escapeHtml(u.city) : ''}</div>
|
||||
</div>
|
||||
<div class="privacy-note" style="font-size:11px;">${u.registered_ago || ''}</div>
|
||||
</div>
|
||||
`).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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user