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:
root 2026-08-21 11:24:47 +00:00
parent 1f02bf04e5
commit a2cc623756
3 changed files with 160 additions and 63 deletions

View File

@ -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>

View File

@ -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); },

View File

@ -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;