caterium-app/public/core/auth-security-v1774.js
pavlov346346-source 736e551369
Security: require verified email for signup
* security: require verified email for client signups

* security: load verified-email auth guard

* test: include auth security guard in deploy checks

* test: guard verified-email signup flow

* security: remove client-controlled email autoconfirm triggers
2026-09-11 07:27:37 +03:00

192 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(()=>{
'use strict';
const VERSION='17.7.3-auth-security-v1';
const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23';
let busy=false;
const $=id=>document.getElementById(id);
const cloud=()=>window.SunCloudV2||null;
const client=()=>cloud()?.getClient?.()||null;
const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??'');
function redirectUrl(){
try{
const u=new URL(location.href);
u.hash='';
return u.toString();
}catch(_){return location.href.split('#')[0];}
}
function savePendingRegistration(email,companyName){
try{
localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({
email:String(email||'').trim().toLowerCase(),
companyName:String(companyName||'').trim()||'Новая компания',
createdAt:new Date().toISOString()
}));
}catch(_){}
}
function clearPendingRegistration(){
try{localStorage.removeItem(PENDING_REGISTRATION_KEY)}catch(_){}
}
function setError(root,message){
const node=root?.querySelector?.('#sunGateErrorV3');
if(node)node.textContent=String(message||'');
}
function friendlySignupError(error){
const message=String(error?.message||error||'Не удалось создать аккаунт.');
if(/email address not authorized/i.test(message)){
return 'Не удалось отправить письмо подтверждения. Обратитесь к администратору Caterium.';
}
if(/rate limit|rate_limit|too many/i.test(message)){
return 'Слишком много попыток регистрации. Попробуйте немного позже.';
}
return message;
}
async function signOutUnsafeSession(c){
try{await c?.auth?.signOut?.()}catch(_){}
}
function showPublicConfirmation(gate,email){
if(!gate)return;
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Подтвердите email</h2><div class="hint">Безопасная регистрация Caterium</div></div></div><p class="hint">Мы отправили письмо на <b>${esc(email)}</b>. Откройте письмо и нажмите ссылку подтверждения. После подтверждения Caterium откроется автоматически.</p><div class="sun-cloud-auth-actions"><button class="primary" id="sunAuthGoLoginV1774" type="button">Перейти ко входу</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div></div>`;
gate.querySelector('#sunAuthGoLoginV1774')?.addEventListener('click',()=>{gate.remove();document.body.classList.remove('sun-cloud-auth-required');setTimeout(()=>window.SunEnterprise?.ensureAuthGate?.(),30);location.reload();},{once:true});
}
function showInviteConfirmation(gate,email){
if(!gate)return;
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Подтвердите email</h2><div class="hint">Приглашение сохранено</div></div></div><p class="hint">Письмо подтверждения отправлено на <b>${esc(email)}</b>. Подтвердите адрес по ссылке в письме. После возврата в Caterium приглашение останется доступно.</p><div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteReloadV1774" type="button">Я подтвердил email</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div></div>`;
gate.querySelector('#sunInviteReloadV1774')?.addEventListener('click',()=>location.reload(),{once:true});
}
async function publicSignup(gate){
if(busy)return;
const c=client();
if(!c){setError(gate,'Облачный сервис не подключён.');return;}
const email=String($('sunGateEmailV3')?.value||'').trim().toLowerCase();
const password=String($('sunGatePasswordV3')?.value||'');
const password2=String($('sunGatePassword2V27')?.value||'');
const companyName=String($('sunGateCompanyV3')?.value||'').trim();
if(!companyName){setError(gate,'Введите название компании.');return;}
if(!email||password.length<6){setError(gate,'Введите email и пароль минимум из 6 символов.');return;}
if(password!==password2){setError(gate,'Пароли не совпадают.');return;}
busy=true;
const button=$('sunGateSubmitV3');if(button)button.disabled=true;
setError(gate,'Создаю безопасный аккаунт…');
savePendingRegistration(email,companyName);
try{
const existing=await c.auth.signInWithPassword({email,password});
if(!existing.error&&existing.data?.session){
location.reload();
return;
}
const result=await c.auth.signUp({
email,
password,
options:{
emailRedirectTo:redirectUrl(),
data:{company_name:companyName}
}
});
if(result.error)throw result.error;
if(result.data?.session){
await signOutUnsafeSession(c);
throw new Error('Защита email ещё не активирована на сервере. Регистрация остановлена, чтобы не создавать неподтверждённый аккаунт.');
}
showPublicConfirmation(gate,email);
}catch(error){
clearPendingRegistration();
setError(gate,friendlySignupError(error));
if(button)button.disabled=false;
}finally{busy=false;}
}
async function acceptInviteAfterLogin(token,gate){
const api=cloud();
const ws=await api?.acceptInvite?.(token);
if(!ws)throw new Error('Не удалось присоединиться к компании.');
try{const u=new URL(location.href);u.searchParams.delete('invite');history.replaceState(null,'',u.toString())}catch(_){}
location.reload();
}
async function inviteSignup(gate){
if(busy)return;
const c=client();
if(!c){setError(gate,'Облачный сервис не подключён.');return;}
const email=String($('sunInviteEmailV27')?.value||'').trim().toLowerCase();
const name=String($('sunInviteNameV27')?.value||'').trim();
const password=String($('sunInvitePasswordV27')?.value||'');
const password2=String($('sunInvitePassword2V27')?.value||'');
let token='';try{token=String(new URL(location.href).searchParams.get('invite')||'').trim()}catch(_){}
if(!token){setError(gate,'Ссылка приглашения повреждена.');return;}
if(!email||password.length<6){setError(gate,'Пароль должен содержать минимум 6 символов.');return;}
if(password!==password2){setError(gate,'Пароли не совпадают.');return;}
busy=true;
const button=$('sunInviteJoinV27');if(button)button.disabled=true;
setError(gate,'Проверяю аккаунт…');
try{
const existing=await c.auth.signInWithPassword({email,password});
if(!existing.error&&existing.data?.session){
await acceptInviteAfterLogin(token,gate);
return;
}
const result=await c.auth.signUp({
email,
password,
options:{
emailRedirectTo:redirectUrl(),
data:{name}
}
});
if(result.error)throw result.error;
if(result.data?.session){
await signOutUnsafeSession(c);
throw new Error('Защита email ещё не активирована на сервере. Приглашение остановлено, чтобы не создавать неподтверждённый аккаунт.');
}
showInviteConfirmation(gate,email);
}catch(error){
setError(gate,friendlySignupError(error));
if(button)button.disabled=false;
}finally{busy=false;}
}
function publicRegistrationMode(){
const fields=$('sunGateRegisterFieldsV27');
return Boolean(fields&&!fields.hidden);
}
document.addEventListener('click',event=>{
const target=event.target instanceof Element?event.target:null;
if(!target)return;
if(target.closest('#sunGateSubmitV3')&&publicRegistrationMode()){
event.preventDefault();event.stopImmediatePropagation();
publicSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error)));
return;
}
if(target.closest('#sunInviteJoinV27')){
event.preventDefault();event.stopImmediatePropagation();
inviteSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error)));
}
},true);
document.addEventListener('keydown',event=>{
if(event.key!=='Enter')return;
const target=event.target instanceof Element?event.target:null;
if(target?.id==='sunGatePassword2V27'&&publicRegistrationMode()){
event.preventDefault();event.stopImmediatePropagation();
publicSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error)));
}else if(target?.id==='sunInvitePassword2V27'){
event.preventDefault();event.stopImmediatePropagation();
inviteSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error)));
}
},true);
window.CateriumAuthSecurityV1774=Object.freeze({VERSION,redirectUrl});
})();