Fix session persistence + полный рерайт регистрации
FIX SESSION:
- Store.save() теперь сохраняет в localStorage ('buhapp_session_v1')
- Store.load() восстанавливает access+refresh
- Store.clear() чистит и память и localStorage
- При загрузке api.js сразу Store.load() — сессия восстанавливается автоматически
- При refresh страницы init() видит Store.access и сразу делает me()
REGISTRATION REDO:
- Отдельный экран showEmailRegister() (не на том же экране что login)
- Поля: имя, email, пароль, пароль2, 18+, соглашения, запомнить
- Полная валидация в реальном времени (имя не пустое, email валидный,
пароль ≥8, пароли совпадают, чекбоксы отмечены)
- Кнопка disabled пока форма не готова, hint показывает что не так
- После успешной регистрации: сразу показ 'Добро пожаловать!' + переход на карту
- Ошибка от сервера — показывается в hint + подсветка поля email красным
- Удалён старый emailRegisterPre и дублирующий emailRegister
This commit is contained in:
parent
1f02bf04e5
commit
a2cc623756
@ -34,15 +34,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<script src="/js/tg.js?v=21"></script>
|
<script src="/js/tg.js?v=22"></script>
|
||||||
<script src="/js/api.js?v=21"></script>
|
<script src="/js/api.js?v=22"></script>
|
||||||
<script src="/js/views/map.js?v=21"></script>
|
<script src="/js/views/map.js?v=22"></script>
|
||||||
<script src="/js/views/chats.js?v=21"></script>
|
<script src="/js/views/chats.js?v=22"></script>
|
||||||
<script src="/js/views/chat.js?v=21"></script>
|
<script src="/js/views/chat.js?v=22"></script>
|
||||||
<script src="/js/views/profile.js?v=21"></script>
|
<script src="/js/views/profile.js?v=22"></script>
|
||||||
<script src="/js/views/settings.js?v=21"></script>
|
<script src="/js/views/settings.js?v=22"></script>
|
||||||
<script src="/js/views/onboarding.js?v=21"></script>
|
<script src="/js/views/onboarding.js?v=22"></script>
|
||||||
<script src="/js/views/review.js?v=21"></script>
|
<script src="/js/views/review.js?v=22"></script>
|
||||||
<script src="/js/app.js?v=21"></script>
|
<script src="/js/app.js?v=22"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -55,15 +55,41 @@ const Store = {
|
|||||||
access: null,
|
access: null,
|
||||||
refresh: null,
|
refresh: null,
|
||||||
user: null,
|
user: null,
|
||||||
|
_key: 'buhapp_session_v1',
|
||||||
save: function (tokens) {
|
save: function (tokens) {
|
||||||
if (!tokens) return;
|
if (!tokens) return;
|
||||||
this.access = tokens.Access || tokens.access;
|
this.access = tokens.Access || tokens.access;
|
||||||
this.refresh = tokens.Refresh || tokens.refresh;
|
this.refresh = tokens.Refresh || tokens.refresh;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(this._key, JSON.stringify({
|
||||||
|
access: this.access,
|
||||||
|
refresh: this.refresh,
|
||||||
|
}));
|
||||||
|
} catch (e) { /* localStorage недоступен (Safari private mode) — игнорим */ }
|
||||||
|
},
|
||||||
|
load: function () {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(this._key);
|
||||||
|
if (!raw) return null;
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
this.access = data.access || null;
|
||||||
|
this.refresh = data.refresh || null;
|
||||||
|
return this.access;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear: function () {
|
||||||
|
this.access = null;
|
||||||
|
this.refresh = null;
|
||||||
|
this.user = null;
|
||||||
|
try { localStorage.removeItem(this._key); } catch (e) {}
|
||||||
},
|
},
|
||||||
load: function () { return this.access; },
|
|
||||||
clear: function () { this.access = null; this.refresh = null; this.user = null; },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// сразу восстановим сессию при загрузке скрипта (нужно для refresh страницы)
|
||||||
|
Store.load();
|
||||||
|
|
||||||
const Auth = {
|
const Auth = {
|
||||||
loginEmail: function (email, password, remember) { return apiPost('/api/v1/auth/login', { email: email, password: password, remember: !!remember }); },
|
loginEmail: function (email, password, remember) { return apiPost('/api/v1/auth/login', { email: email, password: password, remember: !!remember }); },
|
||||||
registerEmail: function (data) { return apiPost('/api/v1/auth/register', data); },
|
registerEmail: function (data) { return apiPost('/api/v1/auth/register', data); },
|
||||||
|
|||||||
173
public/js/app.js
173
public/js/app.js
@ -231,7 +231,7 @@ const App = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.getElementById('el-login').onclick = () => this.emailLogin();
|
document.getElementById('el-login').onclick = () => this.emailLogin();
|
||||||
document.getElementById('el-register').onclick = () => this.emailRegisterPre();
|
document.getElementById('el-register').onclick = () => this.showEmailRegister();
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const email = document.getElementById('el-email').value.trim();
|
const email = document.getElementById('el-email').value.trim();
|
||||||
@ -260,6 +260,127 @@ const App = {
|
|||||||
this.loadRecentUsers();
|
this.loadRecentUsers();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// === ОТДЕЛЬНЫЙ ЭКРАН РЕГИСТРАЦИИ ===
|
||||||
|
showEmailRegister() {
|
||||||
|
document.getElementById('app').innerHTML = `
|
||||||
|
<div class="view">
|
||||||
|
<h1>Регистрация</h1>
|
||||||
|
<div class="sub">Создай аккаунт за 30 секунд</div>
|
||||||
|
<div class="card">
|
||||||
|
<label>Имя (как к тебе обращаться)</label>
|
||||||
|
<input class="input" id="rg-name" type="text" placeholder="Саша" autocomplete="given-name">
|
||||||
|
<label>Email</label>
|
||||||
|
<input class="input" id="rg-email" type="email" placeholder="you@example.com" autocomplete="email">
|
||||||
|
<label>Пароль (≥8 символов)</label>
|
||||||
|
<input class="input" id="rg-pass" type="password" placeholder="минимум 8 символов" autocomplete="new-password">
|
||||||
|
<label>Пароль ещё раз</label>
|
||||||
|
<input class="input" id="rg-pass2" type="password" placeholder="повтори пароль" autocomplete="new-password">
|
||||||
|
<label class="checkrow">
|
||||||
|
<input type="checkbox" id="rg-adult" checked>
|
||||||
|
<span>Мне уже есть 18 лет</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkrow">
|
||||||
|
<input type="checkbox" id="rg-terms" checked>
|
||||||
|
<span>Принимаю <a href="/legal/TERMS.html" target="_blank">соглашение</a></span>
|
||||||
|
</label>
|
||||||
|
<label class="checkrow">
|
||||||
|
<input type="checkbox" id="rg-remember" checked>
|
||||||
|
<span>Запомнить меня (30 дней)</span>
|
||||||
|
</label>
|
||||||
|
<button class="btn" id="rg-submit">Создать аккаунт</button>
|
||||||
|
<button class="btn ghost" id="rg-back">← Назад ко входу</button>
|
||||||
|
<div class="sub" id="rg-hint" style="margin-top:8px;text-align:center;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
const name = document.getElementById('rg-name').value.trim();
|
||||||
|
const email = document.getElementById('rg-email').value.trim();
|
||||||
|
const pass = document.getElementById('rg-pass').value;
|
||||||
|
const pass2 = document.getElementById('rg-pass2').value;
|
||||||
|
const adult = document.getElementById('rg-adult').checked;
|
||||||
|
const terms = document.getElementById('rg-terms').checked;
|
||||||
|
const btn = document.getElementById('rg-submit');
|
||||||
|
const hint = document.getElementById('rg-hint');
|
||||||
|
let msg = '';
|
||||||
|
let ready = true;
|
||||||
|
if (name.length < 1) { msg = 'Введите имя'; ready = false; }
|
||||||
|
else if (!email.includes('@') || !email.includes('.')) { msg = 'Некорректный email'; ready = false; }
|
||||||
|
else if (pass.length < 8) { msg = 'Пароль минимум 8 символов (' + pass.length + '/8)'; ready = false; }
|
||||||
|
else if (pass !== pass2) { msg = 'Пароли не совпадают'; ready = false; }
|
||||||
|
else if (!adult) { msg = 'Только для лиц 18+'; ready = false; }
|
||||||
|
else if (!terms) { msg = 'Примите соглашение'; ready = false; }
|
||||||
|
else { msg = '✓ Всё готово'; }
|
||||||
|
btn.disabled = !ready;
|
||||||
|
hint.textContent = msg;
|
||||||
|
hint.style.color = ready ? 'var(--success)' : (msg.startsWith('✓') ? 'var(--success)' : 'var(--text-dim)');
|
||||||
|
};
|
||||||
|
['rg-name','rg-email','rg-pass','rg-pass2','rg-adult','rg-terms'].forEach((id) => {
|
||||||
|
document.getElementById(id).addEventListener('input', update);
|
||||||
|
document.getElementById(id).addEventListener('change', update);
|
||||||
|
});
|
||||||
|
update();
|
||||||
|
|
||||||
|
document.getElementById('rg-submit').onclick = () => this.emailRegister();
|
||||||
|
document.getElementById('rg-back').onclick = () => this.showEmailLogin();
|
||||||
|
},
|
||||||
|
|
||||||
|
async emailRegister() {
|
||||||
|
const name = document.getElementById('rg-name').value.trim();
|
||||||
|
const email = document.getElementById('rg-email').value.trim();
|
||||||
|
const pass = document.getElementById('rg-pass').value;
|
||||||
|
const pass2 = document.getElementById('rg-pass2').value;
|
||||||
|
const adult = document.getElementById('rg-adult').checked;
|
||||||
|
const terms = document.getElementById('rg-terms').checked;
|
||||||
|
const remember = document.getElementById('rg-remember').checked;
|
||||||
|
const btn = document.getElementById('rg-submit');
|
||||||
|
const hint = document.getElementById('rg-hint');
|
||||||
|
|
||||||
|
// повторная проверка на стороне клиента
|
||||||
|
if (!name || !email.includes('@') || pass.length < 8 || pass !== pass2 || !adult || !terms) {
|
||||||
|
hint.textContent = 'Проверьте поля';
|
||||||
|
hint.style.color = 'var(--danger)';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '⏳ Создаём аккаунт…';
|
||||||
|
hint.textContent = 'Регистрация на сервере…';
|
||||||
|
hint.style.color = 'var(--text-dim)';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await API.Auth.registerEmail({
|
||||||
|
email,
|
||||||
|
password: pass,
|
||||||
|
name,
|
||||||
|
consents: { adult: true, terms: true, privacy: true, disclaimer: true },
|
||||||
|
remember: remember,
|
||||||
|
});
|
||||||
|
if (r && r.tokens) API.Store.save(r.tokens);
|
||||||
|
const me = await API.Auth.me();
|
||||||
|
API.Store.user = me.user || me;
|
||||||
|
btn.textContent = '✓ Готово';
|
||||||
|
hint.textContent = 'Добро пожаловать, ' + name + '!';
|
||||||
|
hint.style.color = 'var(--success)';
|
||||||
|
this.onLogin();
|
||||||
|
await this.maybeOnboard();
|
||||||
|
this.showView('map');
|
||||||
|
} catch (e) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Создать аккаунт';
|
||||||
|
const msg = (e && e.message) || 'Неизвестная ошибка';
|
||||||
|
hint.textContent = '⚠️ ' + msg;
|
||||||
|
hint.style.color = 'var(--danger)';
|
||||||
|
if (msg.includes('email')) {
|
||||||
|
// подсветить поле email красным
|
||||||
|
const el = document.getElementById('rg-email');
|
||||||
|
if (el) el.style.borderColor = 'var(--danger)';
|
||||||
|
}
|
||||||
|
console.error('register failed:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async loadRecentUsers() {
|
async loadRecentUsers() {
|
||||||
try {
|
try {
|
||||||
const r = await api('/api/v1/users/recent?limit=8');
|
const r = await api('/api/v1/users/recent?limit=8');
|
||||||
@ -288,19 +409,6 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Сначала consent overlay, потом реальная регистрация
|
|
||||||
emailRegisterPre() {
|
|
||||||
const email = document.getElementById('el-email').value.trim();
|
|
||||||
const pass = document.getElementById('el-pass').value;
|
|
||||||
if (!email || pass.length < 8) {
|
|
||||||
TG_APP.showAlert('Email и пароль (≥8 символов) обязательны');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// проверим дубликат email
|
|
||||||
this._preReg = () => this.emailRegister();
|
|
||||||
this.showConsent();
|
|
||||||
},
|
|
||||||
|
|
||||||
async emailLogin() {
|
async emailLogin() {
|
||||||
const email = document.getElementById('el-email').value.trim();
|
const email = document.getElementById('el-email').value.trim();
|
||||||
const pass = document.getElementById('el-pass').value;
|
const pass = document.getElementById('el-pass').value;
|
||||||
@ -318,43 +426,6 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async emailRegister() {
|
|
||||||
const email = document.getElementById('el-email').value.trim();
|
|
||||||
const pass = document.getElementById('el-pass').value;
|
|
||||||
// показываем сообщение о процессе
|
|
||||||
const ov = document.createElement('div');
|
|
||||||
ov.id = 'consent-overlay';
|
|
||||||
ov.innerHTML = `
|
|
||||||
<div style="text-align:center;padding:48px 16px;">
|
|
||||||
<div style="font-size:32px;margin-bottom:12px;">⏳</div>
|
|
||||||
<div>Регистрация...</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const target = document.querySelector('body.mobile-mode #mobile-frame') || document.body;
|
|
||||||
target.appendChild(ov);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const remember = document.getElementById('el-remember')?.checked ?? true;
|
|
||||||
const r = await API.Auth.registerEmail({
|
|
||||||
email,
|
|
||||||
password: pass,
|
|
||||||
name: email.split('@')[0],
|
|
||||||
consents: { adult: true, terms: true, privacy: true, disclaimer: true },
|
|
||||||
remember: remember,
|
|
||||||
});
|
|
||||||
if (r && r.tokens) API.Store.save(r.tokens);
|
|
||||||
await API.Auth.me();
|
|
||||||
ov.remove();
|
|
||||||
await this.maybeOnboard();
|
|
||||||
this.showView('map');
|
|
||||||
} catch (e) {
|
|
||||||
ov.remove();
|
|
||||||
TG_APP.showAlert('Ошибка регистрации: ' + (e?.message || 'unknown'));
|
|
||||||
// вернёмся к форме
|
|
||||||
this.showEmailLogin();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async maybeOnboard() {
|
async maybeOnboard() {
|
||||||
if (this._onboardRunning) return;
|
if (this._onboardRunning) return;
|
||||||
this._onboardRunning = true;
|
this._onboardRunning = true;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user