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 = {}) {
|
async function api(path, opts = {}) {
|
||||||
opts.headers = opts.headers || {};
|
opts.headers = opts.headers || {};
|
||||||
// НЕ шлём Authorization — используем HttpOnly cookie (без CORS preflight)
|
// НЕ шлём Authorization — используем HttpOnly cookie (без CORS preflight)
|
||||||
// Middleware читает cookie buhapp_at
|
|
||||||
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
|
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
|
||||||
opts.headers['Content-Type'] = 'application/json';
|
opts.headers['Content-Type'] = 'application/json';
|
||||||
opts.body = JSON.stringify(opts.body);
|
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();
|
const text = await r.text();
|
||||||
let data = null;
|
let data = null;
|
||||||
try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
||||||
@ -60,6 +72,12 @@ async function api(path, opts = {}) {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === 'AbortError') throw new Error('Сервер не отвечает (таймаут 20с)');
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(tid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Auth ===
|
// === Auth ===
|
||||||
|
|||||||
@ -50,7 +50,6 @@ const App = {
|
|||||||
this.showError('Сессия истекла: ' + e.message);
|
this.showError('Сессия истекла: ' + e.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Вне Telegram — показать email login (но сначала согласия)
|
|
||||||
this.showWelcome();
|
this.showWelcome();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,8 +213,12 @@ const App = {
|
|||||||
<label>Пароль (≥8 символов)</label>
|
<label>Пароль (≥8 символов)</label>
|
||||||
<input class="input" id="el-pass" type="password" placeholder="минимум 8 символов" autocomplete="current-password">
|
<input class="input" id="el-pass" type="password" placeholder="минимум 8 символов" autocomplete="current-password">
|
||||||
<button class="btn" id="el-login" disabled>Войти</button>
|
<button class="btn" id="el-login" disabled>Войти</button>
|
||||||
<button class="btn ghost" id="el-register" disabled>Регистрация</button>
|
<button class="btn ghost" id="el-register">Регистрация</button>
|
||||||
<div class="sub" id="el-hint" style="margin-top:8px;text-align:center;">Введите email и пароль</div>
|
<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>
|
||||||
<div class="card" style="font-size:12px;color:var(--text-dim);">
|
<div class="card" style="font-size:12px;color:var(--text-dim);">
|
||||||
Регистрируясь, вы подтверждаете совершеннолетие (18+) и принимаете
|
Регистрируясь, вы подтверждаете совершеннолетие (18+) и принимаете
|
||||||
@ -231,21 +234,54 @@ const App = {
|
|||||||
const pass = document.getElementById('el-pass').value;
|
const pass = document.getElementById('el-pass').value;
|
||||||
const ok = email.length > 0 && pass.length >= 8;
|
const ok = email.length > 0 && pass.length >= 8;
|
||||||
document.getElementById('el-login').disabled = !ok;
|
document.getElementById('el-login').disabled = !ok;
|
||||||
document.getElementById('el-register').disabled = !ok;
|
|
||||||
const hint = document.getElementById('el-hint');
|
const hint = document.getElementById('el-hint');
|
||||||
if (pass.length > 0 && pass.length < 8) {
|
if (pass.length > 0 && pass.length < 8) {
|
||||||
hint.textContent = 'Пароль слишком короткий (' + pass.length + '/8)';
|
hint.textContent = 'Пароль слишком короткий (' + pass.length + '/8)';
|
||||||
hint.style.color = 'var(--danger)';
|
hint.style.color = 'var(--danger)';
|
||||||
} else if (ok) {
|
} else if (ok) {
|
||||||
hint.textContent = '✓ Готово';
|
hint.textContent = '✓ Готово — нажми Войти';
|
||||||
hint.style.color = 'var(--success)';
|
hint.style.color = 'var(--success)';
|
||||||
|
} else if (pass.length > 0) {
|
||||||
|
hint.textContent = 'Введите email';
|
||||||
|
hint.style.color = 'var(--text-dim)';
|
||||||
} else {
|
} else {
|
||||||
hint.textContent = 'Введите email и пароль';
|
hint.textContent = 'Введите email и пароль (≥8 символов)';
|
||||||
hint.style.color = 'var(--text-dim)';
|
hint.style.color = 'var(--text-dim)';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.getElementById('el-email').addEventListener('input', update);
|
document.getElementById('el-email').addEventListener('input', update);
|
||||||
document.getElementById('el-pass').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, потом реальная регистрация
|
// Сначала consent overlay, потом реальная регистрация
|
||||||
@ -266,9 +302,10 @@ const App = {
|
|||||||
const pass = document.getElementById('el-pass').value;
|
const pass = document.getElementById('el-pass').value;
|
||||||
try {
|
try {
|
||||||
await API.Auth.loginEmail(email, pass);
|
await API.Auth.loginEmail(email, pass);
|
||||||
|
// Safari: дать применить Set-Cookie перед следующим fetch
|
||||||
|
await new Promise((r) => setTimeout(r, 150));
|
||||||
await API.Auth.me();
|
await API.Auth.me();
|
||||||
this.onLogin(); // показать таббар
|
this.onLogin();
|
||||||
// для email-логина согласия можно проверить, но юзер их уже дал
|
|
||||||
this.maybeOnboard();
|
this.maybeOnboard();
|
||||||
this.showView('map');
|
this.showView('map');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user