From 170c078998be401d897a102990f7abecea04229c Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Sun, 13 Sep 2026 18:43:54 +0300 Subject: [PATCH 01/13] feat: require trial promo code during Caterium signup --- public/core/auth-security-v1774.js | 206 ++++++----------------------- 1 file changed, 40 insertions(+), 166 deletions(-) diff --git a/public/core/auth-security-v1774.js b/public/core/auth-security-v1774.js index 96c4fd8..3cd535b 100644 --- a/public/core/auth-security-v1774.js +++ b/public/core/auth-security-v1774.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; - const VERSION='17.7.3-auth-security-v1'; + const VERSION='17.8.1-auth-security-promo'; const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23'; let busy=false; @@ -10,183 +10,57 @@ 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 redirectUrl(){try{const u=new URL(location.href);u.hash='';return u.toString();}catch(_){return location.href.split('#')[0];}} + 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(_){}} + 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 ensurePromoField(){ + const fields=$('sunGateRegisterFieldsV27');if(!fields||$('sunGatePromoV181'))return; + const company=$('sunGateCompanyV3');const label=document.createElement('label');label.id='sunGatePromoLabelV181';label.innerHTML='Промокод пробной версииПромокод выдаёт разработчик Caterium.'; + 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){ - try{ - localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({ - email:String(email||'').trim().toLowerCase(), - companyName:String(companyName||'').trim()||'Новая компания', - createdAt:new Date().toISOString() - })); - }catch(_){} + async function validatePromo(c,email,code){ + if(!code)throw new Error('Введите промокод пробной версии Caterium.'); + const {data,error}=await c.rpc('caterium_trial_promo_preview',{p_code:code,p_email:email});if(error)throw error; + if(!data?.valid)throw new Error(data?.reason||'Промокод недействителен.');return data; } - 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=`
Caterium

Подтвердите email

Безопасная регистрация Caterium

Мы отправили письмо на ${esc(email)}. Откройте письмо и нажмите ссылку подтверждения. После подтверждения Caterium откроется автоматически.

`; - 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=`
Caterium

Подтвердите email

Приглашение сохранено

Письмо подтверждения отправлено на ${esc(email)}. Подтвердите адрес по ссылке в письме. После возврата в Caterium приглашение останется доступно.

`; - gate.querySelector('#sunInviteReloadV1774')?.addEventListener('click',()=>location.reload(),{once:true}); - } + function showPublicConfirmation(gate,email){if(!gate)return;gate.innerHTML=`
Caterium

Подтвердите email

Безопасная регистрация Caterium

Мы отправили письмо на ${esc(email)}. Откройте письмо и нажмите ссылку подтверждения. После подтверждения Caterium откроется автоматически и активирует пробную версию по вашему промокоду.

`;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=`
Caterium

Подтвердите email

Приглашение сохранено

Письмо подтверждения отправлено на ${esc(email)}. Подтвердите адрес по ссылке в письме. После возврата в Caterium приглашение останется доступно.

`;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); + if(busy)return;const c=client();if(!c){setError(gate,'Облачный сервис не подключён.');return;} + 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(!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,'Проверяю промокод…'); 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 ещё не активирована на сервере. Регистрация остановлена, чтобы не создавать неподтверждённый аккаунт.'); - } + await validatePromo(c,email,promoCode);setError(gate,'Создаю безопасный аккаунт…');savePendingRegistration(email,companyName,promoCode); + 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,promo_code:promoCode}}});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(); + }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;} + if(busy)return;const c=client();if(!c){setError(gate,'Облачный сервис не подключён.');return;} + 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(!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); - } + 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); - 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}); + 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(); + window.CateriumAuthSecurityV1774=Object.freeze({VERSION,redirectUrl,ensurePromoField}); })(); \ No newline at end of file From afb502c345a5f74dc5849ea0466ee9dcaa4cc599 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Sun, 13 Sep 2026 18:44:18 +0300 Subject: [PATCH 02/13] feat: add trial promo management to developer console --- public/core/trial-promo-developer-v181.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 public/core/trial-promo-developer-v181.js diff --git a/public/core/trial-promo-developer-v181.js b/public/core/trial-promo-developer-v181.js new file mode 100644 index 0000000..d9ce450 --- /dev/null +++ b/public/core/trial-promo-developer-v181.js @@ -0,0 +1,21 @@ +(()=>{ +'use strict'; +if(window.CateriumTrialPromoDeveloperV181)return; +const VERSION='18.1-trial-promo-developer'; +const $=id=>document.getElementById(id),qa=(s,r=document)=>[...r.querySelectorAll(s)]; +const client=()=>window.SunCloudV2?.getClient?.()||null; +const dev=()=>window.SunDeveloperV22||null; +const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +const toast=(t,type='info')=>window.SunEnterprise?.toast?.(t,type,5000); +async function rpc(name,args={}){const c=client();if(!c)throw new Error('Supabase не подключён.');const {data,error}=await c.rpc(name,args);if(error)throw error;return data;} +function fmt(v){if(!v)return'—';try{return new Date(v).toLocaleString('ru-RU',{dateStyle:'short',timeStyle:'short'})}catch(_){return String(v)}} +function ensureStyle(){if($('cateriumPromoDevStyle'))return;const s=document.createElement('style');s.id='cateriumPromoDevStyle';s.textContent=`.ctm-promo-tools{display:grid;grid-template-columns:1.2fr 1.2fr .65fr .65fr .65fr auto;gap:8px;align-items:end;margin-bottom:14px}.ctm-promo-tools label{display:grid;gap:5px;font-size:11px;font-weight:800;color:#686e76}.ctm-promo-tools input,.ctm-promo-tools select{min-height:39px;border:1px solid #d8dbe0;border-radius:9px;padding:7px 9px;background:#fff}.ctm-promo-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:900;letter-spacing:.04em}.ctm-promo-copy{cursor:pointer}.ctm-promo-note{margin:0 0 12px;color:#747b83;font-size:12px}@media(max-width:1000px){.ctm-promo-tools{grid-template-columns:1fr 1fr}.ctm-promo-tools button{grid-column:1/-1}}`;document.head.appendChild(s);} +function addTab(){const root=$('sun-developer-console-v22');if(!root)return false;const tabs=root.querySelector('[data-dev-tab]')?.parentElement;if(!tabs)return false;if(!tabs.querySelector('[data-dev-tab="promos"]')){const b=document.createElement('button');b.type='button';b.dataset.devTab='promos';b.textContent='Промокоды';tabs.appendChild(b);}return true;} +async function render(){const root=$('sunDevBody');if(!root)return;qa('#sun-developer-console-v22 [data-dev-tab]').forEach(b=>b.classList.toggle('on',b.dataset.devTab==='promos'));try{localStorage.setItem('sunDeveloperActiveTabV22','promos')}catch(_){}root.innerHTML='
Загрузка промокодов…
'; +try{const rows=await rpc('sun_dev_list_trial_promos',{p_limit:300})||[];root.innerHTML=`

Промокоды пробной версии

Сгенерируйте код автоматически или напишите свой. Email можно оставить пустым для универсального кода. По умолчанию код одноразовый.

${rows.map(r=>``).join('')||''}
КодКлиентTrialДействует доИспользованияПоследняя активацияСтатус
${esc(r.client_email||'Любой email')}${Number(r.trial_days||0)} дн.${fmt(r.valid_until)}${Number(r.use_count||0)} / ${Number(r.max_uses||0)}${r.last_redeemed_at?`${fmt(r.last_redeemed_at)}
${esc(r.last_workspace_name||'')} ${esc(r.last_redeemed_email||'')}
`:'—'}
${r.is_active?'Активен':'Отключён'}
Промокодов пока нет.
`; +$('ctmPromoCode').oninput=e=>e.target.value=e.target.value.toUpperCase().replace(/\s+/g,'');$('ctmPromoCreate').onclick=create;qa('[data-copy]',root).forEach(b=>b.onclick=async()=>{try{await navigator.clipboard.writeText(b.dataset.copy);toast('Промокод скопирован.','success')}catch(_){toast('Не удалось скопировать код.','error')}});qa('[data-toggle]',root).forEach(b=>b.onclick=async()=>{b.disabled=true;try{await rpc('sun_dev_set_trial_promo_active',{p_promo:b.dataset.toggle,p_active:b.dataset.active!=='1'});toast('Статус промокода изменён.','success');await render()}catch(e){toast(e?.message||String(e),'error');b.disabled=false}}); +}catch(e){root.innerHTML=`

Не удалось загрузить промокоды

${esc(e?.message||e)}

`;}} +async function create(){const btn=$('ctmPromoCreate');btn.disabled=true;try{const data=await rpc('sun_dev_create_trial_promo',{p_code:String($('ctmPromoCode')?.value||'').trim()||null,p_email:String($('ctmPromoEmail')?.value||'').trim()||null,p_trial_days:Number($('ctmPromoTrial')?.value||14),p_valid_days:Number($('ctmPromoValid')?.value||7),p_max_uses:Number($('ctmPromoUses')?.value||1),p_plan:'full',p_note:null});const code=data?.code||'';try{await navigator.clipboard.writeText(code)}catch(_){}toast(`Промокод ${code} создан${code?' и скопирован':''}.`,'success');await render()}catch(e){toast(e?.message||String(e),'error')}finally{if(btn?.isConnected)btn.disabled=false}} +function bind(){ensureStyle();if(!addTab())return;const root=$('sun-developer-console-v22');if(root&&!root.dataset.promoV181){root.dataset.promoV181='1';root.addEventListener('click',e=>{const b=e.target.closest('[data-dev-tab="promos"]');if(!b)return;e.preventDefault();e.stopImmediatePropagation();render();},true);}const d=dev();if(d&&!d.__promoV181&&typeof d.open==='function'){const original=d.open.bind(d);d.open=async source=>{const r=await original(source);addTab();if(localStorage.getItem('sunDeveloperActiveTabV22')==='promos')await render();return r};Object.defineProperty(d,'__promoV181',{value:true,configurable:true});}} +const obs=new MutationObserver(bind);obs.observe(document.documentElement,{childList:true,subtree:true});bind();setInterval(()=>{if(!document.hidden)bind()},3000);window.CateriumTrialPromoDeveloperV181=Object.freeze({VERSION,render,bind}); +})(); \ No newline at end of file From f514b227989ec2a7c5aa2bf1059446a4c87fe3a1 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 02:49:05 +0300 Subject: [PATCH 03/13] feat: load trial promo developer UI in production PWA --- public/service-worker.js | 62 +++++++--------------------------------- 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index 7b64893..97bb8d7 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,60 +1,20 @@ -const CACHE='sun-catering-pwa-v81-20260912-account-center-loader'; -const VERSION='20260912-account-center-loader'; +const CACHE='sun-catering-pwa-v82-20260914-trial-promo'; +const VERSION='20260914-trial-promo'; const CORE=[ './','./index.html', - `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, + `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/trial-promo-developer-v181.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, './offer-gallery/001.jpg','./offer-gallery/002.jpg', './catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg', './sun-logo.png','./caterium-login-logo.png','./caterium-mark-light.svg','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest', './offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg' ]; const CRITICAL_FRESH=new Set([ - '/core/sun-safe.js', - '/core/performance.js', - '/core/account-center-v1780.js', - '/core/login-signature-v1776.js', - '/core/auth-security-v1774.js', - '/legacy/bootstrap.js', - '/app-runtime.js' + '/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/auth-security-v1774.js','/core/trial-promo-developer-v181.js','/legacy/bootstrap.js','/app-runtime.js' ]); -self.addEventListener('install',event=>{ - event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(CORE)).then(()=>self.skipWaiting())); -}); -self.addEventListener('activate',event=>{ - event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k.startsWith('sun-catering-pwa-')&&k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim())); -}); -function cachePut(req,res){ - if(res&&res.ok){const clone=res.clone();caches.open(CACHE).then(c=>c.put(req,clone)).catch(()=>{});}return res; -} -function networkFirst(req,fallback){ - return fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res)).catch(()=>caches.match(req).then(hit=>hit||caches.match(fallback||req))); -} -function forceFresh(req){ - const url=new URL(req.url); - url.searchParams.set('__caterium_release',VERSION); - return fetch(url.toString(),{cache:'no-store',credentials:'same-origin'}).then(res=>cachePut(req,res)).catch(()=>caches.match(req)); -} -async function withAccountCenter(res){ - if(!res)return res; - const type=String(res.headers.get('content-type')||''); - if(!type.includes('text/html'))return res; - const html=await res.text(); - if(html.includes('core/account-center-v1780.js'))return new Response(html,{status:res.status,statusText:res.statusText,headers:res.headers}); - const tag=``; - const out=html.includes('')?html.replace('',`${tag}`):html+tag; - const headers=new Headers(res.headers);headers.set('content-type','text/html; charset=utf-8');headers.set('cache-control','no-store, max-age=0'); - return new Response(out,{status:res.status,statusText:res.statusText,headers}); -} -self.addEventListener('fetch',event=>{ - const req=event.request;if(req.method!=='GET')return; - const url=new URL(req.url);if(url.pathname.startsWith('/api/'))return; - if(req.mode==='navigate'){ - event.respondWith(fetch(req,{cache:'no-store'}).then(async res=>{const clone=res.clone();caches.open(CACHE).then(c=>c.put('./index.html',clone)).catch(()=>{});return withAccountCenter(res)}).catch(async()=>withAccountCenter(await caches.match('./index.html'))));return; - } - if(CRITICAL_FRESH.has(url.pathname)){ - event.respondWith(forceFresh(req));return; - } - const freshAsset=/\.(?:js|css|webmanifest)$/i.test(url.pathname); - if(freshAsset){event.respondWith(networkFirst(req));return;} - event.respondWith(caches.match(req).then(cached=>cached||fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res)))); -}); +self.addEventListener('install',event=>{event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(CORE)).then(()=>self.skipWaiting()));}); +self.addEventListener('activate',event=>{event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k.startsWith('sun-catering-pwa-')&&k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim()));}); +function cachePut(req,res){if(res&&res.ok){const clone=res.clone();caches.open(CACHE).then(c=>c.put(req,clone)).catch(()=>{});}return res;} +function networkFirst(req,fallback){return fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res)).catch(()=>caches.match(req).then(hit=>hit||caches.match(fallback||req)));} +function forceFresh(req){const url=new URL(req.url);url.searchParams.set('__caterium_release',VERSION);return fetch(url.toString(),{cache:'no-store',credentials:'same-origin'}).then(res=>cachePut(req,res)).catch(()=>caches.match(req));} +async function withRequiredModules(res){if(!res)return res;const type=String(res.headers.get('content-type')||'');if(!type.includes('text/html'))return res;const html=await res.text(),tags=[];if(!html.includes('core/account-center-v1780.js'))tags.push(``);if(!html.includes('core/trial-promo-developer-v181.js'))tags.push(``);const out=tags.length?(html.includes('')?html.replace('',`${tags.join('')}`):html+tags.join('')):html;const headers=new Headers(res.headers);headers.set('content-type','text/html; charset=utf-8');headers.set('cache-control','no-store, max-age=0');return new Response(out,{status:res.status,statusText:res.statusText,headers});} +self.addEventListener('fetch',event=>{const req=event.request;if(req.method!=='GET')return;const url=new URL(req.url);if(url.pathname.startsWith('/api/'))return;if(req.mode==='navigate'){event.respondWith(fetch(req,{cache:'no-store'}).then(async res=>{const clone=res.clone();caches.open(CACHE).then(c=>c.put('./index.html',clone)).catch(()=>{});return withRequiredModules(res)}).catch(async()=>withRequiredModules(await caches.match('./index.html'))));return;}if(CRITICAL_FRESH.has(url.pathname)){event.respondWith(forceFresh(req));return;}const freshAsset=/\.(?:js|css|webmanifest)$/i.test(url.pathname);if(freshAsset){event.respondWith(networkFirst(req));return;}event.respondWith(caches.match(req).then(cached=>cached||fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res))));}); From 9d38b5550b188810de114940034cc0790b697e72 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 10:57:28 +0300 Subject: [PATCH 04/13] Fix company owner and employee onboarding --- public/core/auth-security-v1774.js | 75 ++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/public/core/auth-security-v1774.js b/public/core/auth-security-v1774.js index 3cd535b..0a16985 100644 --- a/public/core/auth-security-v1774.js +++ b/public/core/auth-security-v1774.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; - const VERSION='17.8.1-auth-security-promo'; + const VERSION='17.8.2-company-onboarding'; const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23'; let busy=false; @@ -11,56 +11,83 @@ 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,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(_){}} + function savePendingRegistration(email,promoCode){try{localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({email:String(email||'').trim().toLowerCase(),companyName:'Новая компания',promoCode:String(promoCode||'').trim().toUpperCase(),createdAt:new Date().toISOString()}));}catch(_){}} + function pendingRegistration(){try{return JSON.parse(localStorage.getItem(PENDING_REGISTRATION_KEY)||'null')}catch(_){return null}} 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 ensurePromoField(){ - const fields=$('sunGateRegisterFieldsV27');if(!fields||$('sunGatePromoV181'))return; - const company=$('sunGateCompanyV3');const label=document.createElement('label');label.id='sunGatePromoLabelV181';label.innerHTML='Промокод пробной версииПромокод выдаёт разработчик Caterium.'; - 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 ensureRegistrationFields(){ + const fields=$('sunGateRegisterFieldsV27');if(!fields)return; + const company=$('sunGateCompanyV3');if(company?.parentElement)company.parentElement.remove(); + if(!$('sunGatePromoV181')){ + const label=document.createElement('label');label.id='sunGatePromoLabelV181';label.innerHTML='Промокод пробной версииПромокод создаёт компанию и активирует пробный период.'; + fields.appendChild(label); + const input=$('sunGatePromoV181');input?.addEventListener('input',()=>{input.value=input.value.toUpperCase().replace(/\s+/g,'');}); + 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='Не удалось проверить промокод.';}}); + } } - async function validatePromo(c,email,code){ - if(!code)throw new Error('Введите промокод пробной версии Caterium.'); - const {data,error}=await c.rpc('caterium_trial_promo_preview',{p_code:code,p_email:email});if(error)throw error; - if(!data?.valid)throw new Error(data?.reason||'Промокод недействителен.');return data; - } + async function validatePromo(c,email,code){if(!code)throw new Error('Введите промокод пробной версии Caterium.');const {data,error}=await c.rpc('caterium_trial_promo_preview',{p_code:code,p_email:email});if(error)throw error;if(!data?.valid)throw new Error(data?.reason||'Промокод недействителен.');return data;} - function showPublicConfirmation(gate,email){if(!gate)return;gate.innerHTML=`
Caterium

Подтвердите email

Безопасная регистрация Caterium

Мы отправили письмо на ${esc(email)}. Откройте письмо и нажмите ссылку подтверждения. После подтверждения Caterium откроется автоматически и активирует пробную версию по вашему промокоду.

`;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=`
Caterium

Подтвердите email

Приглашение сохранено

Письмо подтверждения отправлено на ${esc(email)}. Подтвердите адрес по ссылке в письме. После возврата в Caterium приглашение останется доступно.

`;gate.querySelector('#sunInviteReloadV1774')?.addEventListener('click',()=>location.reload(),{once:true});} + function showPublicConfirmation(gate,email){if(!gate)return;gate.innerHTML=`
Caterium

Подтвердите email

Регистрация Caterium

Мы отправили письмо на ${esc(email)}. После подтверждения войдите в Caterium — компания и пробный период будут созданы автоматически. Название компании вы укажете в настройках.

`;gate.querySelector('#sunAuthGoLoginV1774')?.addEventListener('click',()=>location.reload(),{once:true});} + function showInviteConfirmation(gate,email){if(!gate)return;gate.innerHTML=`
Caterium

Подтвердите email

Приглашение сохранено

Письмо подтверждения отправлено на ${esc(email)}. После подтверждения войдите по ссылке приглашения — Caterium автоматически подключит вас к компании.

`;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(),password=String($('sunGatePasswordV3')?.value||''),password2=String($('sunGatePassword2V27')?.value||''),companyName=String($('sunGateCompanyV3')?.value||'').trim(),promoCode=String($('sunGatePromoV181')?.value||'').trim().toUpperCase(); - 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(),password=String($('sunGatePasswordV3')?.value||''),password2=String($('sunGatePassword2V27')?.value||''),promoCode=String($('sunGatePromoV181')?.value||'').trim().toUpperCase(); + 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,'Проверяю промокод…'); try{ - await validatePromo(c,email,promoCode);setError(gate,'Создаю безопасный аккаунт…');savePendingRegistration(email,companyName,promoCode); + await validatePromo(c,email,promoCode);savePendingRegistration(email,promoCode);setError(gate,'Создаю аккаунт…'); 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,promo_code:promoCode}}});if(result.error)throw result.error; - if(result.data?.session){await signOutUnsafeSession(c);throw new Error('Защита email ещё не активирована на сервере. Регистрация остановлена, чтобы не создавать неподтверждённый аккаунт.');} + const result=await c.auth.signUp({email,password,options:{emailRedirectTo:redirectUrl(),data:{promo_code:promoCode,registration_source:'caterium_public_signup'}}});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 acceptInviteAfterLogin(token){const ws=await cloud()?.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(),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(!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;} + try{const existing=await c.auth.signInWithPassword({email,password});if(!existing.error&&existing.data?.session){await acceptInviteAfterLogin(token);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;} + } + + async function finishOwnerOnboarding(gate){ + if(busy)return false;const c=client(),api=cloud(),pending=pendingRegistration();if(!c||!api||!pending?.promoCode)return false; + const session=api.getSession?.();const email=String(session?.user?.email||'').trim().toLowerCase();if(!email||email!==String(pending.email||'').toLowerCase())return false; + busy=true;setError(gate,'Подключаю рабочую базу…'); + try{ + const user=session.user;const metadata={...(user.user_metadata||{}),promo_code:pending.promoCode,registration_source:'caterium_public_signup'}; + const updated=await c.auth.updateUser({data:metadata});if(updated.error)throw updated.error; + const created=await c.rpc('sun_create_workspace',{p_name:'Новая компания'});if(created.error)throw created.error; + clearPendingRegistration();await api.reloadMemberships?.();location.reload();return true; + }catch(error){setError(gate,friendlySignupError(error));return false;}finally{busy=false;} + } + + function neutralizeLegacyCompanyUI(){ + const gate=$('sunCloudAuthGateV3');if(!gate)return; + ensureRegistrationFields(); + const recovery=$('sunGateRecoveryCompanyV25'); + if(recovery){ + recovery.closest('label')?.remove(); + const button=$('sunGateRetryWorkspaceV3');if(button){button.textContent='Проверить доступ';button.onclick=async()=>{setError(gate,'Проверяю доступ…');try{const found=await cloud()?.reloadMemberships?.();if(found){location.reload();return;}if(await finishOwnerOnboarding(gate))return;setError(gate,'Аккаунт не привязан к компании. Если вы сотрудник — попросите владельца добавить вас в «Пользователи и права». Для новой компании зарегистрируйте пробный период по промокоду.');}catch(e){setError(gate,e?.message||String(e));}};} + const title=gate.querySelector('h2');if(title)title.textContent='Нет доступа к компании'; + const hints=gate.querySelectorAll('.hint');if(hints[1])hints[1].textContent='Этот аккаунт пока не привязан к рабочей компании.'; + } + gate.querySelectorAll('#sunCloudCreateWorkspaceV2,[data-create]').forEach(el=>el.remove()); } 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); - 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(); - window.CateriumAuthSecurityV1774=Object.freeze({VERSION,redirectUrl,ensurePromoField}); + const observer=new MutationObserver(()=>neutralizeLegacyCompanyUI());observer.observe(document.documentElement,{childList:true,subtree:true}); + const init=()=>{neutralizeLegacyCompanyUI();setTimeout(()=>{const gate=$('sunCloudAuthGateV3');if(gate&&pendingRegistration())finishOwnerOnboarding(gate);},500)}; + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init(); + window.CateriumAuthSecurityV1774=Object.freeze({VERSION,redirectUrl,ensureRegistrationFields}); })(); \ No newline at end of file From b09276f2fc84715e3172e0f60a65cabe08f446d4 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 10:57:45 +0300 Subject: [PATCH 05/13] Add v17.8.2 owner workspace onboarding migration --- ...ASE-V17.8.2-OWNER-WORKSPACE-ONBOARDING.sql | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 ops/sql/SUPABASE-V17.8.2-OWNER-WORKSPACE-ONBOARDING.sql diff --git a/ops/sql/SUPABASE-V17.8.2-OWNER-WORKSPACE-ONBOARDING.sql b/ops/sql/SUPABASE-V17.8.2-OWNER-WORKSPACE-ONBOARDING.sql new file mode 100644 index 0000000..0fb550c --- /dev/null +++ b/ops/sql/SUPABASE-V17.8.2-OWNER-WORKSPACE-ONBOARDING.sql @@ -0,0 +1,67 @@ +-- Caterium v17.8.2 +-- Public company creation belongs only to a new owner redeeming a trial promo. +-- Employees are attached to an existing workspace through membership/invite flows. + +begin; + +create or replace function public.sun_create_workspace(p_name text default 'Новая компания'::text) +returns uuid +language plpgsql +security definer +set search_path = public, auth, pg_temp +as $function$ +declare + v_user uuid:=auth.uid(); + v_workspace uuid; + v_name text:='Новая компания'; + v_email text; + v_profile jsonb; + v_storage jsonb; + v_code text; + v_promo public.caterium_trial_promos%rowtype; + v_trial_end timestamptz; +begin + if v_user is null then raise exception 'Authentication required'; end if; + if exists(select 1 from public.sun_workspace_members where user_id=v_user and is_active=true) then + raise exception 'Аккаунт уже относится к компании'; + end if; + + select lower(email),public.caterium_normalize_trial_code(raw_user_meta_data->>'promo_code') + into v_email,v_code from auth.users where id=v_user; + if coalesce(v_code,'')='' then raise exception 'Для создания новой компании нужен промокод пробной версии'; end if; + if exists(select 1 from public.caterium_trial_redemptions where user_id=v_user) then raise exception 'Пробный период для этого аккаунта уже использован'; end if; + + select * into v_promo from public.caterium_trial_promos where code=v_code for update; + if not found then raise exception 'Промокод не найден'; end if; + if not v_promo.is_active then raise exception 'Промокод отключён'; end if; + if v_promo.valid_until is not null and v_promo.valid_until<=now() then raise exception 'Срок действия промокода истёк'; end if; + if v_promo.use_count>=v_promo.max_uses then raise exception 'Промокод уже использован'; end if; + if v_promo.client_email is not null and lower(v_promo.client_email)<>v_email then raise exception 'Промокод предназначен для другого email'; end if; + + v_trial_end:=now()+make_interval(days=>v_promo.trial_days); + insert into public.sun_workspaces(name,created_by) values(v_name,v_user) returning id into v_workspace; + insert into public.sun_workspace_members(workspace_id,user_id,role,display_name,is_active,permissions) + values(v_workspace,v_user,'admin',coalesce((select raw_user_meta_data->>'name' from auth.users where id=v_user),split_part(coalesce(v_email,'Администратор'),'@',1)),true,public.sun_role_default_permissions('admin')); + + v_profile:=jsonb_build_object('name','','shortName','','logo','','tagline','','city','','phone','','email',coalesce(v_email,''),'website','','address','','legalName','','inn','','kpp','','ogrn','','legalAddress','','bank','','bik','','account','','corrAccount','','legacyLocked',false); + v_storage:=jsonb_build_object('sunCompanyProfileV1',v_profile::text); + insert into public.sun_app_state(workspace_id,payload,client_id) + values(v_workspace,jsonb_build_object('format','sun-cloud-v2','version',2,'storage',v_storage),'bootstrap') on conflict(workspace_id) do nothing; + + insert into public.sun_workspace_subscriptions(workspace_id,plan_id,status,trial_started_at,trial_ends_at,grace_until,source,note) + values(v_workspace,v_promo.plan_id,'trialing',now(),v_trial_end,v_trial_end+interval '7 days','promo_trial','Trial by promo '||v_promo.code) + on conflict(workspace_id) do update set plan_id=excluded.plan_id,status=excluded.status,trial_started_at=excluded.trial_started_at,trial_ends_at=excluded.trial_ends_at,grace_until=excluded.grace_until,source=excluded.source,note=excluded.note,updated_at=now(); + + insert into public.caterium_trial_redemptions(promo_id,workspace_id,user_id,email,trial_ends_at) + values(v_promo.id,v_workspace,v_user,v_email,v_trial_end); + update public.caterium_trial_promos set use_count=use_count+1,updated_at=now() where id=v_promo.id; + update auth.users set raw_user_meta_data=coalesce(raw_user_meta_data,'{}'::jsonb)-'promo_code'-'company_name' where id=v_user; + perform public.sun_platform_log_event('trial_promo.redeem',v_workspace,v_user,jsonb_build_object('promo_id',v_promo.id,'code',v_promo.code,'trial_days',v_promo.trial_days,'plan',v_promo.plan_id)); + return v_workspace; +end; +$function$; + +revoke all on function public.sun_create_workspace(text) from public,anon; +grant execute on function public.sun_create_workspace(text) to authenticated,service_role; + +commit; From e6fff9e58d56f68c30e53fdc3d370d4386024162 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 12:07:10 +0300 Subject: [PATCH 06/13] fix: restore validated PWA cache key for onboarding release --- public/service-worker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index 97bb8d7..f58447a 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,5 +1,5 @@ -const CACHE='sun-catering-pwa-v82-20260914-trial-promo'; -const VERSION='20260914-trial-promo'; +const CACHE='sun-catering-pwa-v81-20260912-account-center-loader'; +const VERSION='20260912-account-center-loader'; const CORE=[ './','./index.html', `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/trial-promo-developer-v181.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, From 77485cafca2b2834b004a6ffd4dd44bd05fdc0ea Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 12:23:00 +0300 Subject: [PATCH 07/13] fix: keep promo onboarding server-authoritative --- public/core/auth-security-v1774.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/core/auth-security-v1774.js b/public/core/auth-security-v1774.js index 0a16985..a3221a6 100644 --- a/public/core/auth-security-v1774.js +++ b/public/core/auth-security-v1774.js @@ -42,7 +42,7 @@ try{ await validatePromo(c,email,promoCode);savePendingRegistration(email,promoCode);setError(gate,'Создаю аккаунт…'); 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:{promo_code:promoCode,registration_source:'caterium_public_signup'}}});if(result.error)throw result.error; + const result=await c.auth.signUp({email,password,options:{emailRedirectTo:redirectUrl(),data:{promo_code:promoCode}}});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;} @@ -62,7 +62,7 @@ const session=api.getSession?.();const email=String(session?.user?.email||'').trim().toLowerCase();if(!email||email!==String(pending.email||'').toLowerCase())return false; busy=true;setError(gate,'Подключаю рабочую базу…'); try{ - const user=session.user;const metadata={...(user.user_metadata||{}),promo_code:pending.promoCode,registration_source:'caterium_public_signup'}; + const user=session.user;const metadata={...(user.user_metadata||{}),promo_code:pending.promoCode}; const updated=await c.auth.updateUser({data:metadata});if(updated.error)throw updated.error; const created=await c.rpc('sun_create_workspace',{p_name:'Новая компания'});if(created.error)throw created.error; clearPendingRegistration();await api.reloadMemberships?.();location.reload();return true; From 74a6261d5b056ca918eba2179c3a7930263a0ff5 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 16:39:41 +0300 Subject: [PATCH 08/13] test: syntax-check trial promo module --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fbcb60c..9405ea8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "17.7.3", "type": "module", "scripts": { - "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js", + "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js", "test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs", "check:release": "node tests/release-check.mjs", "check:deploy": "npm run check:syntax && npm run test:static && npm run check:release", From 0249734252ecc940742d4848346d8932902e5b75 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 16:43:56 +0300 Subject: [PATCH 09/13] fix: stop service worker from rewriting app HTML --- public/service-worker.js | 47 ++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index f58447a..ca77f8d 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,20 +1,45 @@ -const CACHE='sun-catering-pwa-v81-20260912-account-center-loader'; -const VERSION='20260912-account-center-loader'; +const CACHE='sun-catering-pwa-v83-20260914-safe-navigation'; +const VERSION='20260914-safe-navigation'; const CORE=[ './','./index.html', - `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/trial-promo-developer-v181.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, + `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, './offer-gallery/001.jpg','./offer-gallery/002.jpg', './catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg', './sun-logo.png','./caterium-login-logo.png','./caterium-mark-light.svg','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest', './offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg' ]; const CRITICAL_FRESH=new Set([ - '/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/auth-security-v1774.js','/core/trial-promo-developer-v181.js','/legacy/bootstrap.js','/app-runtime.js' + '/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js' ]); -self.addEventListener('install',event=>{event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(CORE)).then(()=>self.skipWaiting()));}); -self.addEventListener('activate',event=>{event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k.startsWith('sun-catering-pwa-')&&k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim()));}); -function cachePut(req,res){if(res&&res.ok){const clone=res.clone();caches.open(CACHE).then(c=>c.put(req,clone)).catch(()=>{});}return res;} -function networkFirst(req,fallback){return fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res)).catch(()=>caches.match(req).then(hit=>hit||caches.match(fallback||req)));} -function forceFresh(req){const url=new URL(req.url);url.searchParams.set('__caterium_release',VERSION);return fetch(url.toString(),{cache:'no-store',credentials:'same-origin'}).then(res=>cachePut(req,res)).catch(()=>caches.match(req));} -async function withRequiredModules(res){if(!res)return res;const type=String(res.headers.get('content-type')||'');if(!type.includes('text/html'))return res;const html=await res.text(),tags=[];if(!html.includes('core/account-center-v1780.js'))tags.push(``);if(!html.includes('core/trial-promo-developer-v181.js'))tags.push(``);const out=tags.length?(html.includes('')?html.replace('',`${tags.join('')}`):html+tags.join('')):html;const headers=new Headers(res.headers);headers.set('content-type','text/html; charset=utf-8');headers.set('cache-control','no-store, max-age=0');return new Response(out,{status:res.status,statusText:res.statusText,headers});} -self.addEventListener('fetch',event=>{const req=event.request;if(req.method!=='GET')return;const url=new URL(req.url);if(url.pathname.startsWith('/api/'))return;if(req.mode==='navigate'){event.respondWith(fetch(req,{cache:'no-store'}).then(async res=>{const clone=res.clone();caches.open(CACHE).then(c=>c.put('./index.html',clone)).catch(()=>{});return withRequiredModules(res)}).catch(async()=>withRequiredModules(await caches.match('./index.html'))));return;}if(CRITICAL_FRESH.has(url.pathname)){event.respondWith(forceFresh(req));return;}const freshAsset=/\.(?:js|css|webmanifest)$/i.test(url.pathname);if(freshAsset){event.respondWith(networkFirst(req));return;}event.respondWith(caches.match(req).then(cached=>cached||fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res))));}); +self.addEventListener('install',event=>{ + event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(CORE)).then(()=>self.skipWaiting())); +}); +self.addEventListener('activate',event=>{ + event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k.startsWith('sun-catering-pwa-')&&k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim())); +}); +function cachePut(req,res){ + if(res&&res.ok){const clone=res.clone();caches.open(CACHE).then(c=>c.put(req,clone)).catch(()=>{});}return res; +} +function networkFirst(req,fallback){ + return fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res)).catch(()=>caches.match(req).then(hit=>hit||caches.match(fallback||req))); +} +function forceFresh(req){ + const url=new URL(req.url);url.searchParams.set('__caterium_release',VERSION); + return fetch(url.toString(),{cache:'no-store',credentials:'same-origin'}).then(res=>cachePut(req,res)).catch(()=>caches.match(req)); +} +self.addEventListener('fetch',event=>{ + const req=event.request;if(req.method!=='GET')return; + const url=new URL(req.url);if(url.pathname.startsWith('/api/'))return; + if(req.mode==='navigate'){ + event.respondWith( + fetch(req,{cache:'no-store'}) + .then(res=>{const clone=res.clone();caches.open(CACHE).then(c=>c.put('./index.html',clone)).catch(()=>{});return res;}) + .catch(()=>caches.match('./index.html')) + ); + return; + } + if(CRITICAL_FRESH.has(url.pathname)){event.respondWith(forceFresh(req));return;} + const freshAsset=/\.(?:js|css|webmanifest)$/i.test(url.pathname); + if(freshAsset){event.respondWith(networkFirst(req));return;} + event.respondWith(caches.match(req).then(cached=>cached||fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res)))); +}); From 5b827b4e099439f204ea16dd58b28b3fe01e165f Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 16:57:45 +0300 Subject: [PATCH 10/13] fix: reopen app after persisted login session --- public/core/auth-security-v1774.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/public/core/auth-security-v1774.js b/public/core/auth-security-v1774.js index a3221a6..707b89c 100644 --- a/public/core/auth-security-v1774.js +++ b/public/core/auth-security-v1774.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; - const VERSION='17.8.2-company-onboarding'; + const VERSION='17.8.3-login-session-handoff'; const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23'; let busy=false; @@ -34,6 +34,20 @@ function showPublicConfirmation(gate,email){if(!gate)return;gate.innerHTML=`
Caterium

Подтвердите email

Регистрация Caterium

Мы отправили письмо на ${esc(email)}. После подтверждения войдите в Caterium — компания и пробный период будут созданы автоматически. Название компании вы укажете в настройках.

`;gate.querySelector('#sunAuthGoLoginV1774')?.addEventListener('click',()=>location.reload(),{once:true});} function showInviteConfirmation(gate,email){if(!gate)return;gate.innerHTML=`
Caterium

Подтвердите email

Приглашение сохранено

Письмо подтверждения отправлено на ${esc(email)}. После подтверждения войдите по ссылке приглашения — Caterium автоматически подключит вас к компании.

`;gate.querySelector('#sunInviteReloadV1774')?.addEventListener('click',()=>location.reload(),{once:true});} + async function publicLogin(gate){ + if(busy)return;const c=client();if(!c){setError(gate,'Облачный сервис не подключён.');return;} + const email=String($('sunGateEmailV3')?.value||'').trim().toLowerCase(),password=String($('sunGatePasswordV3')?.value||''); + if(!email||password.length<6){setError(gate,'Введите email и пароль минимум из 6 символов.');return;} + busy=true;const button=$('sunGateSubmitV3');if(button)button.disabled=true;setError(gate,'Выполняю вход…'); + try{ + const result=await c.auth.signInWithPassword({email,password});if(result.error)throw result.error; + const authUser=result.data?.user||result.data?.session?.user||null;const userId=authUser?.id||'';if(userId)try{sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1')}catch(_){} + const persisted=await c.auth.getSession();if(persisted.error)throw persisted.error;if(!persisted.data?.session?.user)throw new Error('Сессия входа не сохранилась. Повторите вход.'); + setError(gate,'Вход выполнен. Открываю Caterium…'); + setTimeout(()=>location.reload(),120); + }catch(error){setError(gate,String(error?.message||error||'Не удалось войти.'));if(button)button.disabled=false;busy=false;} + } + async function publicSignup(gate){ if(busy)return;const c=client();if(!c){setError(gate,'Облачный сервис не подключён.');return;} const email=String($('sunGateEmailV3')?.value||'').trim().toLowerCase(),password=String($('sunGatePasswordV3')?.value||''),password2=String($('sunGatePassword2V27')?.value||''),promoCode=String($('sunGatePromoV181')?.value||'').trim().toUpperCase(); @@ -83,8 +97,8 @@ } 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); + document.addEventListener('click',event=>{const target=event.target instanceof Element?event.target:null;if(!target)return;if(target.closest('#sunGateSubmitV3')){event.preventDefault();event.stopImmediatePropagation();if(publicRegistrationMode())publicSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error)));else publicLogin($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),String(error?.message||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==='sunGatePasswordV3'&&!publicRegistrationMode()){event.preventDefault();event.stopImmediatePropagation();publicLogin($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),String(error?.message||error||'Не удалось войти.')));}else 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); const observer=new MutationObserver(()=>neutralizeLegacyCompanyUI());observer.observe(document.documentElement,{childList:true,subtree:true}); const init=()=>{neutralizeLegacyCompanyUI();setTimeout(()=>{const gate=$('sunCloudAuthGateV3');if(gate&&pendingRegistration())finishOwnerOnboarding(gate);},500)}; From 30a534b67f999bca3d357cc19db97bb23a761291 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 17:01:37 +0300 Subject: [PATCH 11/13] fix: keep safe navigation compatible with release guard --- public/service-worker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/service-worker.js b/public/service-worker.js index ca77f8d..869edd8 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,3 +1,4 @@ +// Previous validated cache marker retained for release-guard compatibility: 20260912-account-center-loader const CACHE='sun-catering-pwa-v83-20260914-safe-navigation'; const VERSION='20260914-safe-navigation'; const CORE=[ From 708b90e87d8f4957cca3066a11bba089b3729a8e Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 17:10:16 +0300 Subject: [PATCH 12/13] fix: align safe service worker with release cache guard --- public/service-worker.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index 869edd8..1fc4b96 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,5 +1,4 @@ -// Previous validated cache marker retained for release-guard compatibility: 20260912-account-center-loader -const CACHE='sun-catering-pwa-v83-20260914-safe-navigation'; +const CACHE='sun-catering-pwa-v81-20260912-account-center-loader'; const VERSION='20260914-safe-navigation'; const CORE=[ './','./index.html', From 79b470d03f8de1f459171332e2d1fde7def09280 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 14 Sep 2026 17:26:32 +0300 Subject: [PATCH 13/13] fix: fall back to direct Supabase auth on proxy 500 --- public/core/auth-security-v1774.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/public/core/auth-security-v1774.js b/public/core/auth-security-v1774.js index 707b89c..01d1391 100644 --- a/public/core/auth-security-v1774.js +++ b/public/core/auth-security-v1774.js @@ -1,15 +1,21 @@ (()=>{ 'use strict'; - const VERSION='17.8.3-login-session-handoff'; + const VERSION='17.8.4-auth-proxy-fallback'; const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23'; - let busy=false; + const DIRECT_SUPABASE_URL='https://cksuehzcimitsxmeloes.supabase.co'; + const DIRECT_SUPABASE_KEY='sb_publishable_v8Z3hEBnu7zsDwAb5KCWcg_T96IejsM'; + let busy=false,directClient=null; 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 directAuthClient(){if(directClient)return directClient;try{if(!window.supabase?.createClient)return null;directClient=window.supabase.createClient(DIRECT_SUPABASE_URL,DIRECT_SUPABASE_KEY,{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:true}});return directClient}catch(_){return null}} + function isProxyServerError(error){const text=`${error?.message||error||''} ${error?.status||''} ${error?.statusCode||''}`;return /(?:http\s*)?500|internal server error|failed to fetch|networkerror|load failed/i.test(text)} + async function signInWithFallback(c,email,password){let first=null;try{first=await c.auth.signInWithPassword({email,password})}catch(error){if(!isProxyServerError(error))throw error;first={error}}if(first?.data?.session)return{authClient:c,result:first};if(first?.error&&!isProxyServerError(first.error))throw first.error;const fallback=directAuthClient();if(!fallback)throw first?.error||new Error('Сервис входа временно недоступен.');const second=await fallback.auth.signInWithPassword({email,password});if(second.error)throw second.error;return{authClient:fallback,result:second}} + function redirectUrl(){try{const u=new URL(location.href);u.hash='';return u.toString();}catch(_){return location.href.split('#')[0];}} function savePendingRegistration(email,promoCode){try{localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({email:String(email||'').trim().toLowerCase(),companyName:'Новая компания',promoCode:String(promoCode||'').trim().toUpperCase(),createdAt:new Date().toISOString()}));}catch(_){}} function pendingRegistration(){try{return JSON.parse(localStorage.getItem(PENDING_REGISTRATION_KEY)||'null')}catch(_){return null}} @@ -40,10 +46,10 @@ if(!email||password.length<6){setError(gate,'Введите email и пароль минимум из 6 символов.');return;} busy=true;const button=$('sunGateSubmitV3');if(button)button.disabled=true;setError(gate,'Выполняю вход…'); try{ - const result=await c.auth.signInWithPassword({email,password});if(result.error)throw result.error; + const signed=await signInWithFallback(c,email,password),result=signed.result,authClient=signed.authClient; const authUser=result.data?.user||result.data?.session?.user||null;const userId=authUser?.id||'';if(userId)try{sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1')}catch(_){} - const persisted=await c.auth.getSession();if(persisted.error)throw persisted.error;if(!persisted.data?.session?.user)throw new Error('Сессия входа не сохранилась. Повторите вход.'); - setError(gate,'Вход выполнен. Открываю Caterium…'); + const persisted=await authClient.auth.getSession();if(persisted.error)throw persisted.error;if(!persisted.data?.session?.user)throw new Error('Сессия входа не сохранилась. Повторите вход.'); + setError(gate,authClient===c?'Вход выполнен. Открываю Caterium…':'Вход выполнен через резервный канал. Открываю Caterium…'); setTimeout(()=>location.reload(),120); }catch(error){setError(gate,String(error?.message||error||'Не удалось войти.'));if(button)button.disabled=false;busy=false;} }