feat: require trial promo code during Caterium signup

This commit is contained in:
pavlov346346-source 2026-09-13 18:43:54 +03:00
parent 834557e865
commit 170c078998

View File

@ -1,7 +1,7 @@
(()=>{ (()=>{
'use strict'; 'use strict';
const VERSION='17.7.3-auth-security-v1'; const VERSION='17.8.1-auth-security-promo';
const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23'; const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23';
let busy=false; let busy=false;
@ -10,183 +10,57 @@
const client=()=>cloud()?.getClient?.()||null; const client=()=>cloud()?.getClient?.()||null;
const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??''); const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??'');
function redirectUrl(){ function redirectUrl(){try{const u=new URL(location.href);u.hash='';return u.toString();}catch(_){return location.href.split('#')[0];}}
try{ function savePendingRegistration(email,companyName,promoCode){try{localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({email:String(email||'').trim().toLowerCase(),companyName:String(companyName||'').trim()||'Новая компания',promoCode:String(promoCode||'').trim().toUpperCase(),createdAt:new Date().toISOString()}));}catch(_){}}
const u=new URL(location.href); function clearPendingRegistration(){try{localStorage.removeItem(PENDING_REGISTRATION_KEY)}catch(_){}}
u.hash=''; function setError(root,message){const node=root?.querySelector?.('#sunGateErrorV3');if(node)node.textContent=String(message||'');}
return u.toString(); 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;}
}catch(_){return location.href.split('#')[0];} async function signOutUnsafeSession(c){try{await c?.auth?.signOut?.()}catch(_){}}
function ensurePromoField(){
const fields=$('sunGateRegisterFieldsV27');if(!fields||$('sunGatePromoV181'))return;
const company=$('sunGateCompanyV3');const label=document.createElement('label');label.id='sunGatePromoLabelV181';label.innerHTML='<span>Промокод пробной версии</span><input id="sunGatePromoV181" autocomplete="off" maxlength="32" placeholder="Например, CTM-A7K4P9"><small id="sunGatePromoHintV181" style="display:block;margin-top:5px;opacity:.72">Промокод выдаёт разработчик Caterium.</small>';
if(company?.parentElement)company.parentElement.insertAdjacentElement('afterend',label);else fields.appendChild(label);
const input=$('sunGatePromoV181');input?.addEventListener('input',()=>{input.value=input.value.toUpperCase().replace(/\s+/g,'');const h=$('sunGatePromoHintV181');if(h)h.textContent='Промокод выдаёт разработчик Caterium.';});
input?.addEventListener('blur',async()=>{const code=String(input.value||'').trim(),email=String($('sunGateEmailV3')?.value||'').trim().toLowerCase(),h=$('sunGatePromoHintV181');if(!code||!h)return;try{const c=client();if(!c)return;const {data,error}=await c.rpc('caterium_trial_promo_preview',{p_code:code,p_email:email||null});if(error)throw error;h.textContent=data?.valid?`Промокод принят · ${Number(data.trial_days||14)} дней пробного доступа`:(data?.reason||'Промокод недействителен');}catch(_){h.textContent='Не удалось проверить промокод.';}});
} }
function savePendingRegistration(email,companyName){ async function validatePromo(c,email,code){
try{ if(!code)throw new Error('Введите промокод пробной версии Caterium.');
localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({ const {data,error}=await c.rpc('caterium_trial_promo_preview',{p_code:code,p_email:email});if(error)throw error;
email:String(email||'').trim().toLowerCase(), if(!data?.valid)throw new Error(data?.reason||'Промокод недействителен.');return data;
companyName:String(companyName||'').trim()||'Новая компания',
createdAt:new Date().toISOString()
}));
}catch(_){}
} }
function clearPendingRegistration(){ 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});}
try{localStorage.removeItem(PENDING_REGISTRATION_KEY)}catch(_){} 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});}
}
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){ async function publicSignup(gate){
if(busy)return; if(busy)return;const c=client();if(!c){setError(gate,'Облачный сервис не подключён.');return;}
const c=client(); const email=String($('sunGateEmailV3')?.value||'').trim().toLowerCase(),password=String($('sunGatePasswordV3')?.value||''),password2=String($('sunGatePassword2V27')?.value||''),companyName=String($('sunGateCompanyV3')?.value||'').trim(),promoCode=String($('sunGatePromoV181')?.value||'').trim().toUpperCase();
if(!c){setError(gate,'Облачный сервис не подключён.');return;} if(!companyName){setError(gate,'Введите название компании.');return;}if(!email||password.length<6){setError(gate,'Введите email и пароль минимум из 6 символов.');return;}if(password!==password2){setError(gate,'Пароли не совпадают.');return;}
const email=String($('sunGateEmailV3')?.value||'').trim().toLowerCase(); busy=true;const button=$('sunGateSubmitV3');if(button)button.disabled=true;setError(gate,'Проверяю промокод…');
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{ try{
const existing=await c.auth.signInWithPassword({email,password}); await validatePromo(c,email,promoCode);setError(gate,'Создаю безопасный аккаунт…');savePendingRegistration(email,companyName,promoCode);
if(!existing.error&&existing.data?.session){ const existing=await c.auth.signInWithPassword({email,password});if(!existing.error&&existing.data?.session){location.reload();return;}
location.reload(); const result=await c.auth.signUp({email,password,options:{emailRedirectTo:redirectUrl(),data:{company_name:companyName,promo_code:promoCode}}});if(result.error)throw result.error;
return; if(result.data?.session){await signOutUnsafeSession(c);throw new Error('Защита email ещё не активирована на сервере. Регистрация остановлена, чтобы не создавать неподтверждённый аккаунт.');}
}
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); showPublicConfirmation(gate,email);
}catch(error){ }catch(error){clearPendingRegistration();setError(gate,friendlySignupError(error));if(button)button.disabled=false;}finally{busy=false;}
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 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){ async function inviteSignup(gate){
if(busy)return; if(busy)return;const c=client();if(!c){setError(gate,'Облачный сервис не подключён.');return;}
const c=client(); const email=String($('sunInviteEmailV27')?.value||'').trim().toLowerCase(),name=String($('sunInviteNameV27')?.value||'').trim(),password=String($('sunInvitePasswordV27')?.value||''),password2=String($('sunInvitePassword2V27')?.value||'');let token='';try{token=String(new URL(location.href).searchParams.get('invite')||'').trim()}catch(_){}
if(!c){setError(gate,'Облачный сервис не подключён.');return;} if(!token){setError(gate,'Ссылка приглашения повреждена.');return;}if(!email||password.length<6){setError(gate,'Пароль должен содержать минимум 6 символов.');return;}if(password!==password2){setError(gate,'Пароли не совпадают.');return;}
const email=String($('sunInviteEmailV27')?.value||'').trim().toLowerCase(); busy=true;const button=$('sunInviteJoinV27');if(button)button.disabled=true;setError(gate,'Проверяю аккаунт…');
const name=String($('sunInviteNameV27')?.value||'').trim(); 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;}
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(){ function publicRegistrationMode(){const fields=$('sunGateRegisterFieldsV27');return Boolean(fields&&!fields.hidden);}
const fields=$('sunGateRegisterFieldsV27'); 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);
return Boolean(fields&&!fields.hidden); 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);
}
document.addEventListener('click',event=>{ const promoObserver=new MutationObserver(()=>ensurePromoField());promoObserver.observe(document.documentElement,{childList:true,subtree:true});if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',ensurePromoField,{once:true});else ensurePromoField();
const target=event.target instanceof Element?event.target:null; window.CateriumAuthSecurityV1774=Object.freeze({VERSION,redirectUrl,ensurePromoField});
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});
})(); })();