fix: remove the racy forced password-change gate for new employees

showPasswordChangeGate() replaced the auth gate's content and returned,
but the same login also fires onAuthStateChange, which kicks off
loadMemberships() independently. Once that resolved (workspace found),
ensureAuthGate()'s periodic poll (every 5s, plus assorted event-driven
calls) saw signedIn+workspace and tore the gate down immediately,
regardless of whether a password-change form was currently blocking
it - the employee would see the "create a new password" screen flash
and vanish before they could use it, dropping them straight into the
app still on their temporary password.

Removes the forced gate entirely (mustChangePassword/
showPasswordChangeGate) and adds a "Пароль" box to the account
settings card instead, so changing your password is a deliberate,
always-available action rather than a one-shot dialog racing the rest
of the login flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
pavlov346346-source 2026-09-13 01:59:36 +03:00
parent 4afde880d2
commit aafad18d6a

View File

@ -2897,6 +2897,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
${connected&&!signed?`<div class="sun-cloud-v2-box wide"><h3>Войти в профиль</h3><div class="sun-cloud-v2-row"><label>Email<input id="sunCloudEmailV2" type="email" autocomplete="username"></label><label>Пароль<input id="sunCloudPasswordV2" type="password" autocomplete="current-password"></label></div><div class="sun-cloud-v2-actions"><button class="primary" id="sunCloudSignInV2" type="button">Войти</button><button class="outline" id="sunCloudSignUpV2" type="button">Зарегистрироваться</button></div></div>`:''}
${!connected?`<div class="sun-cloud-v2-box wide"><h3>Подключение недоступно</h3><p class="sun-cloud-v2-note">Техническое подключение настраивается только из кабинета разработчика.</p></div>`:''}
${signed?`<div class="sun-cloud-v2-box wide"><h3>Профиль</h3><div class="sun-cloud-v2-row"><span class="sun-cloud-v2-user">${esc(session.user.email||session.user.id)}</span><button class="outline" id="sunCloudSignOutV2" type="button">Выйти</button></div></div>`:''}
${signed?`<div class="sun-cloud-v2-box wide"><h3>Пароль</h3><div class="sun-cloud-v2-row"><label>Новый пароль<input id="sunAccountNewPasswordV1" type="password" autocomplete="new-password" minlength="8"></label><label>Подтвердите пароль<input id="sunAccountNewPassword2V1" type="password" autocomplete="new-password" minlength="8"></label></div><div class="sun-cloud-v2-actions"><button class="outline" id="sunAccountChangePasswordV1" type="button">Изменить пароль</button></div></div>`:''}
${signed&&!selected?`<div class="sun-cloud-v2-box wide"><h3>Рабочая компания</h3><p class="sun-cloud-v2-note">${membershipsLoading?'Загружаю доступные рабочие базы…':'У этого аккаунта пока нет доступной рабочей базы.'}</p><div class="sun-cloud-v2-actions"><button class="outline" id="sunCloudReloadMembershipsV2" type="button">Проверить ещё раз</button><button class="primary" id="sunCloudCreateWorkspaceV2" type="button">Создать новую компанию</button></div></div>`:''}
${signed&&selected?`<div class="sun-cloud-v2-box wide"><h3>Рабочая компания</h3><div><b>${esc(workspace.name)}</b>${supportMode?'<span class="sun-cloud-v2-note" style="margin-left:8px">· поддержка, только просмотр</span>':''}</div><div class="sun-cloud-v2-note">Роль: ${supportMode?'Разработчик · поддержка':esc(roleLabel(workspace.role))}${workspace.display_name?` · ${esc(workspace.display_name)}`:''}</div>${!supportMode&&memberships.length>1?`<label>Компания<select id="sunCloudWorkspaceSelectV2">${memberships.map(m=>`<option value="${esc(m.id)}" ${m.id===workspace.id?'selected':''}>${esc(m.name)} · ${esc(roleLabel(m.role))}</option>`).join('')}</select></label>`:''}</div>`:''}
${signed&&selected&&!supportMode?`<div class="sun-cloud-v2-box wide"><h3>Синхронизация</h3><p class="sun-cloud-v2-note">Принудительно загрузить актуальные заказы, клиентов и остальные данные с сервера, заменив локальную копию на этом устройстве. Перед загрузкой автоматически создаётся резервная копия.</p><div class="sun-cloud-v2-actions"><button class="outline" id="sunCloudPullV2" type="button">Загрузить из облака</button></div></div>`:''}
@ -2914,6 +2915,15 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
$('sunCloudSignInV2')?.addEventListener('click',()=>signIn(false));
$('sunCloudSignUpV2')?.addEventListener('click',()=>signIn(true));
$('sunCloudSignOutV2')?.addEventListener('click',signOutAndReload);
$('sunAccountChangePasswordV1')?.addEventListener('click',async()=>{
const p1i=$('sunAccountNewPasswordV1'),p2i=$('sunAccountNewPassword2V1');
const p1=String(p1i?.value||''),p2=String(p2i?.value||'');
if(p1.length<8)return toast('Новый пароль должен содержать минимум 8 символов.','warn');
if(p1!==p2)return toast('Пароли не совпадают.','warn');
if(!client)return toast('Облачный сервис не подключён.','error');
try{const r=await client.auth.updateUser({password:p1});if(r.error)throw r.error;if(p1i)p1i.value='';if(p2i)p2i.value='';toast('Пароль изменён.','success');}
catch(e){toast(String(e?.message||e||'Не удалось изменить пароль.'),'error');}
});
$('sunCloudCreateWorkspaceV2')?.addEventListener('click',createWorkspace);
$('sunCloudReloadMembershipsV2')?.addEventListener('click',async()=>{const tenantSwitched=await loadMemberships();if(tenantSwitched){location.reload();return;}renderCloudUI();if(workspace){subscribeRealtime();await pullRemote({replace:true,quiet:true});}});
$('sunCloudMigrateV2')?.addEventListener('click',migrateLocalToCloud);
@ -3564,14 +3574,12 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
bindPasswordEyes(gate);const btn=gate.querySelector('#sunInviteJoinV27');const run=async()=>{const c=client(),email=String(preview.email||'').trim(),name=String(gate.querySelector('#sunInviteNameV27')?.value||'').trim(),password=String(gate.querySelector('#sunInvitePasswordV27')?.value||''),password2=String(gate.querySelector('#sunInvitePassword2V27')?.value||''),err=gate.querySelector('#sunGateErrorV3');if(password.length<6){err.textContent='Пароль должен содержать минимум 6 символов.';return;}if(password!==password2){err.textContent='Пароли не совпадают.';return;}btn.disabled=true;err.textContent='Проверяю аккаунт…';try{let r=await c.auth.signInWithPassword({email,password});if(r.error){r=await c.auth.signUp({email,password,options:{data:{name,registration_source:'caterium_invite_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error)throw new Error('Этот email уже зарегистрирован. Введите пароль от существующего аккаунта.');r=retry;}}const userId=r.data.user?.id||r.data.session?.user?.id||'';if(userId)sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1');err.textContent='Аккаунт готов. Подключаю компанию…';setTimeout(()=>finishInvite(token,gate),180);}catch(e){err.textContent=String(e?.message||e||'Не удалось войти или создать аккаунт.');btn.disabled=false;}};btn.onclick=run;gate.querySelector('#sunInvitePassword2V27').addEventListener('keydown',e=>{if(e.key==='Enter')run()});
}
function mustChangePassword(user){return Boolean(user?.user_metadata?.must_change_password);}
function showPasswordChangeGate(gate,c,user,onDone){
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Создайте новый пароль</h2><div class="hint">Первый вход сотрудника</div></div></div><p class="hint">Временный пароль сработал. Теперь придумайте свой постоянный пароль.</p>${passwordField('sunEmployeeNewPasswordV28','Новый пароль','new-password')}${passwordField('sunEmployeeNewPassword2V28','Подтвердите пароль','new-password')}<div class="sun-cloud-auth-actions"><button class="primary" id="sunEmployeeSavePasswordV28" type="button">Сохранить новый пароль</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div></div>`;
bindPasswordEyes(gate);
const run=async()=>{const p1=String(gate.querySelector('#sunEmployeeNewPasswordV28')?.value||''),p2=String(gate.querySelector('#sunEmployeeNewPassword2V28')?.value||''),err=gate.querySelector('#sunGateErrorV3'),btn=gate.querySelector('#sunEmployeeSavePasswordV28');if(p1.length<8){err.textContent='Новый пароль должен содержать минимум 8 символов.';return;}if(p1!==p2){err.textContent='Пароли не совпадают.';return;}btn.disabled=true;err.textContent='Сохраняю пароль…';try{const nextData={...(user?.user_metadata||{}),must_change_password:false};const r=await c.auth.updateUser({password:p1,data:nextData});if(r.error)throw r.error;err.textContent='Пароль сохранён.';setTimeout(()=>onDone?.(),120);}catch(e){err.textContent=String(e?.message||e||'Не удалось изменить пароль.');btn.disabled=false;}};
gate.querySelector('#sunEmployeeSavePasswordV28').onclick=run;gate.querySelector('#sunEmployeeNewPassword2V28').addEventListener('keydown',e=>{if(e.key==='Enter')run();});
}
// Password changes now happen from account settings (see cloud-sync-v2.js's
// renderCloudUI "Пароль" box) instead of a forced post-login gate: the old
// gate raced with membership loading (loadMemberships finishing and
// ensureAuthGate's periodic poll tearing the gate down as soon as
// signedIn+workspace was true) and could disappear before the employee
// ever saw it.
function ensureAuthGate(){
const st=cloud()?.status?.();if(!st?.connected)return;
@ -3593,7 +3601,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2 id="sunGateTitleV3">Вход в Caterium</h2><div class="hint" id="sunGateSubtitleV3">Введите данные своего аккаунта</div></div></div><div id="sunGateRegisterFieldsV27" hidden><label>Название компании<input id="sunGateCompanyV3" autocomplete="organization" placeholder="Например, Мой Кейтеринг"></label></div><label>Email<input id="sunGateEmailV3" type="email" autocomplete="username"></label>${passwordField('sunGatePasswordV3','Пароль','current-password')}<div id="sunGateConfirmWrapV27" hidden>${passwordField('sunGatePassword2V27','Подтвердите пароль','new-password')}</div><div class="sun-cloud-auth-actions"><button class="primary" id="sunGateSubmitV3" type="button">Войти</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div><p class="sun-auth-switch"><span id="sunGateSwitchTextV27">Нет аккаунта?</span> <button type="button" id="sunGateSwitchV27">Создать аккаунт</button></p></div>`;document.body.appendChild(gate);bindPasswordEyes(gate);
const emailInput=gate.querySelector('#sunGateEmailV3'),passwordInput=gate.querySelector('#sunGatePasswordV3'),password2Input=gate.querySelector('#sunGatePassword2V27'),companyInput=gate.querySelector('#sunGateCompanyV3'),submitBtn=gate.querySelector('#sunGateSubmitV3'),gateError=gate.querySelector('#sunGateErrorV3');let registerMode=false;
const setMode=(registration)=>{registerMode=Boolean(registration);gate.querySelector('#sunGateRegisterFieldsV27').hidden=!registerMode;gate.querySelector('#sunGateConfirmWrapV27').hidden=!registerMode;gate.querySelector('#sunGateTitleV3').textContent=registerMode?'Создать аккаунт Caterium':'Вход в Caterium';gate.querySelector('#sunGateSubtitleV3').textContent=registerMode?'Заполните данные — после регистрации Caterium откроется автоматически':'Введите данные своего аккаунта';submitBtn.textContent=registerMode?'Создать аккаунт':'Войти';gate.querySelector('#sunGateSwitchTextV27').textContent=registerMode?'Уже есть аккаунт?':'Нет аккаунта?';gate.querySelector('#sunGateSwitchV27').textContent=registerMode?'Войти':'Создать аккаунт';passwordInput.autocomplete=registerMode?'new-password':'current-password';gateError.textContent='';if(registerMode)setTimeout(()=>companyInput.focus(),30);else setTimeout(()=>emailInput.focus(),30)};
async function auth(){const c=client(),email=emailInput.value.trim(),password=passwordInput.value,password2=password2Input.value,companyName=String(companyInput.value||'').trim();if(!c){gateError.textContent='Облачный сервис не подключён.';return;}if(registerMode&&!companyName){gateError.textContent='Введите название компании.';return;}if(!email||password.length<6){gateError.textContent='Введите email и пароль минимум из 6 символов.';return;}if(registerMode&&password!==password2){gateError.textContent='Пароли не совпадают.';return;}submitBtn.disabled=true;gateError.textContent=registerMode?'Создаю аккаунт…':'Выполняю вход…';try{let r;if(registerMode){savePendingRegistration(email,companyName);const existing=await c.auth.signInWithPassword({email,password});if(!existing.error&&existing.data.session){r=existing;}else{r=await c.auth.signUp({email,password,options:{data:{company_name:companyName,registration_source:'caterium_public_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error){clearPendingRegistration();throw new Error('Этот email уже зарегистрирован. Войдите в аккаунт или восстановите пароль.');}r=retry;}}}else r=await c.auth.signInWithPassword({email,password});if(r.error)throw r.error;const authUser=r.data.user||r.data.session?.user||null;const userId=authUser?.id||'';if(userId)sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1');if(!registerMode&&mustChangePassword(authUser)){showPasswordChangeGate(gate,c,authUser,()=>{gate.remove();document.body.classList.remove('sun-cloud-auth-required');setTimeout(()=>ensureAuthGate(),80);});return;}gateError.textContent=registerMode?'Аккаунт готов. Создаю компанию…':'Вход выполнен. Загружаю рабочую базу…';setTimeout(async()=>{await cloud()?.reloadMemberships?.();if(registerMode&&workspace()){clearPendingRegistration();gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();return;}if(getPendingRegistration(email)&&!workspace()){await finishPendingRegistration(gate,email);return;}if(workspace()){gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();}else{gate.remove();ensureAuthGate();}},180);}catch(err){gateError.textContent=String(err?.message||err||'Не удалось выполнить операцию.')}finally{submitBtn.disabled=false}}
async function auth(){const c=client(),email=emailInput.value.trim(),password=passwordInput.value,password2=password2Input.value,companyName=String(companyInput.value||'').trim();if(!c){gateError.textContent='Облачный сервис не подключён.';return;}if(registerMode&&!companyName){gateError.textContent='Введите название компании.';return;}if(!email||password.length<6){gateError.textContent='Введите email и пароль минимум из 6 символов.';return;}if(registerMode&&password!==password2){gateError.textContent='Пароли не совпадают.';return;}submitBtn.disabled=true;gateError.textContent=registerMode?'Создаю аккаунт…':'Выполняю вход…';try{let r;if(registerMode){savePendingRegistration(email,companyName);const existing=await c.auth.signInWithPassword({email,password});if(!existing.error&&existing.data.session){r=existing;}else{r=await c.auth.signUp({email,password,options:{data:{company_name:companyName,registration_source:'caterium_public_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error){clearPendingRegistration();throw new Error('Этот email уже зарегистрирован. Войдите в аккаунт или восстановите пароль.');}r=retry;}}}else r=await c.auth.signInWithPassword({email,password});if(r.error)throw r.error;const authUser=r.data.user||r.data.session?.user||null;const userId=authUser?.id||'';if(userId)sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1');gateError.textContent=registerMode?'Аккаунт готов. Создаю компанию…':'Вход выполнен. Загружаю рабочую базу…';setTimeout(async()=>{await cloud()?.reloadMemberships?.();if(registerMode&&workspace()){clearPendingRegistration();gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();return;}if(getPendingRegistration(email)&&!workspace()){await finishPendingRegistration(gate,email);return;}if(workspace()){gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();}else{gate.remove();ensureAuthGate();}},180);}catch(err){gateError.textContent=String(err?.message||err||'Не удалось выполнить операцию.')}finally{submitBtn.disabled=false}}
gate.querySelector('#sunGateSwitchV27').onclick=()=>setMode(!registerMode);submitBtn.onclick=auth;passwordInput.addEventListener('keydown',e=>{if(e.key==='Enter'&&!registerMode)auth()});password2Input.addEventListener('keydown',e=>{if(e.key==='Enter'&&registerMode)auth()});setMode(false);
}