diff --git a/ops/sql/SUPABASE-V17.7.4-AUTH-EMAIL-VERIFICATION.sql b/ops/sql/SUPABASE-V17.7.4-AUTH-EMAIL-VERIFICATION.sql new file mode 100644 index 0000000..e513c97 --- /dev/null +++ b/ops/sql/SUPABASE-V17.7.4-AUTH-EMAIL-VERIFICATION.sql @@ -0,0 +1,12 @@ +-- Caterium v17.7.4: require real Supabase email verification for client-created accounts. +-- The removed triggers trusted raw_user_meta_data.registration_source, which is client-controlled. + +begin; + +drop trigger if exists caterium_autoconfirm_signup_v25 on auth.users; +drop trigger if exists caterium_autoverify_identity_v25 on auth.identities; + +drop function if exists public.caterium_autoconfirm_signup_v25(); +drop function if exists public.caterium_autoverify_identity_v25(); + +commit; diff --git a/package.json b/package.json index 0f9941e..4f7ddc3 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "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/performance.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", + "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/performance.js && node --check public/core/auth-security-v1774.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", "check:release": "node tests/release-check.mjs", "check:deploy": "npm run check:syntax && npm run test:static && npm run check:release", "test:e2e": "playwright test --config=tests/playwright.config.mjs", diff --git a/public/core/auth-security-v1774.js b/public/core/auth-security-v1774.js new file mode 100644 index 0000000..96c4fd8 --- /dev/null +++ b/public/core/auth-security-v1774.js @@ -0,0 +1,192 @@ +(()=>{ + 'use strict'; + + const VERSION='17.7.3-auth-security-v1'; + const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23'; + let busy=false; + + const $=id=>document.getElementById(id); + const cloud=()=>window.SunCloudV2||null; + const client=()=>cloud()?.getClient?.()||null; + const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??''); + + function redirectUrl(){ + try{ + const u=new URL(location.href); + u.hash=''; + return u.toString(); + }catch(_){return location.href.split('#')[0];} + } + + function savePendingRegistration(email,companyName){ + try{ + localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({ + email:String(email||'').trim().toLowerCase(), + companyName:String(companyName||'').trim()||'Новая компания', + createdAt:new Date().toISOString() + })); + }catch(_){} + } + + function clearPendingRegistration(){ + try{localStorage.removeItem(PENDING_REGISTRATION_KEY)}catch(_){} + } + + function setError(root,message){ + const node=root?.querySelector?.('#sunGateErrorV3'); + if(node)node.textContent=String(message||''); + } + + function friendlySignupError(error){ + const message=String(error?.message||error||'Не удалось создать аккаунт.'); + if(/email address not authorized/i.test(message)){ + return 'Не удалось отправить письмо подтверждения. Обратитесь к администратору Caterium.'; + } + if(/rate limit|rate_limit|too many/i.test(message)){ + return 'Слишком много попыток регистрации. Попробуйте немного позже.'; + } + return message; + } + + async function signOutUnsafeSession(c){ + try{await c?.auth?.signOut?.()}catch(_){} + } + + function showPublicConfirmation(gate,email){ + if(!gate)return; + gate.innerHTML=`
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); + try{ + const existing=await c.auth.signInWithPassword({email,password}); + if(!existing.error&&existing.data?.session){ + location.reload(); + return; + } + const result=await c.auth.signUp({ + email, + password, + options:{ + emailRedirectTo:redirectUrl(), + data:{company_name:companyName} + } + }); + if(result.error)throw result.error; + if(result.data?.session){ + await signOutUnsafeSession(c); + throw new Error('Защита email ещё не активирована на сервере. Регистрация остановлена, чтобы не создавать неподтверждённый аккаунт.'); + } + showPublicConfirmation(gate,email); + }catch(error){ + clearPendingRegistration(); + setError(gate,friendlySignupError(error)); + if(button)button.disabled=false; + }finally{busy=false;} + } + + async function acceptInviteAfterLogin(token,gate){ + const api=cloud(); + const ws=await api?.acceptInvite?.(token); + if(!ws)throw new Error('Не удалось присоединиться к компании.'); + try{const u=new URL(location.href);u.searchParams.delete('invite');history.replaceState(null,'',u.toString())}catch(_){} + location.reload(); + } + + async function inviteSignup(gate){ + if(busy)return; + const c=client(); + if(!c){setError(gate,'Облачный сервис не подключён.');return;} + const email=String($('sunInviteEmailV27')?.value||'').trim().toLowerCase(); + const name=String($('sunInviteNameV27')?.value||'').trim(); + const password=String($('sunInvitePasswordV27')?.value||''); + const password2=String($('sunInvitePassword2V27')?.value||''); + let token='';try{token=String(new URL(location.href).searchParams.get('invite')||'').trim()}catch(_){} + if(!token){setError(gate,'Ссылка приглашения повреждена.');return;} + if(!email||password.length<6){setError(gate,'Пароль должен содержать минимум 6 символов.');return;} + if(password!==password2){setError(gate,'Пароли не совпадают.');return;} + + busy=true; + const button=$('sunInviteJoinV27');if(button)button.disabled=true; + setError(gate,'Проверяю аккаунт…'); + try{ + const existing=await c.auth.signInWithPassword({email,password}); + if(!existing.error&&existing.data?.session){ + await acceptInviteAfterLogin(token,gate); + return; + } + const result=await c.auth.signUp({ + email, + password, + options:{ + emailRedirectTo:redirectUrl(), + data:{name} + } + }); + if(result.error)throw result.error; + if(result.data?.session){ + await signOutUnsafeSession(c); + throw new Error('Защита email ещё не активирована на сервере. Приглашение остановлено, чтобы не создавать неподтверждённый аккаунт.'); + } + showInviteConfirmation(gate,email); + }catch(error){ + setError(gate,friendlySignupError(error)); + if(button)button.disabled=false; + }finally{busy=false;} + } + + function publicRegistrationMode(){ + const fields=$('sunGateRegisterFieldsV27'); + return Boolean(fields&&!fields.hidden); + } + + document.addEventListener('click',event=>{ + const target=event.target instanceof Element?event.target:null; + if(!target)return; + if(target.closest('#sunGateSubmitV3')&&publicRegistrationMode()){ + event.preventDefault();event.stopImmediatePropagation(); + publicSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error))); + return; + } + if(target.closest('#sunInviteJoinV27')){ + event.preventDefault();event.stopImmediatePropagation(); + inviteSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error))); + } + },true); + + document.addEventListener('keydown',event=>{ + if(event.key!=='Enter')return; + const target=event.target instanceof Element?event.target:null; + if(target?.id==='sunGatePassword2V27'&&publicRegistrationMode()){ + event.preventDefault();event.stopImmediatePropagation(); + publicSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error))); + }else if(target?.id==='sunInvitePassword2V27'){ + event.preventDefault();event.stopImmediatePropagation(); + inviteSignup($('sunCloudAuthGateV3')).catch(error=>setError($('sunCloudAuthGateV3'),friendlySignupError(error))); + } + },true); + + window.CateriumAuthSecurityV1774=Object.freeze({VERSION,redirectUrl}); +})(); \ No newline at end of file diff --git a/public/core/performance.js b/public/core/performance.js index d9db3df..5dd6f80 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -119,14 +119,18 @@ if(window.SunOfferWorkspaceV1769||document.getElementById('sunOfferWorkspaceV1769Script'))return; const script=document.createElement('script');script.id='sunOfferWorkspaceV1769Script';script.src=`core/offer-workspace-v1769.js?v=${RELEASE}`;script.async=true;script.onerror=()=>console.error('[Caterium] Не загрузился модуль offer-workspace-v1769.js');document.head.appendChild(script); } + function loadAuthSecurity(){ + if(window.CateriumAuthSecurityV1774||document.getElementById('cateriumAuthSecurityV1774Script'))return; + const script=document.createElement('script');script.id='cateriumAuthSecurityV1774Script';script.src=`core/auth-security-v1774.js?v=${RELEASE}`;script.async=false;script.onerror=()=>console.error('[Caterium] Не загрузился модуль auth-security-v1774.js');document.head.appendChild(script); + } const start=()=>{ - loadDataLayer();loadServerAutomation();loadHotfix();loadOpsUX();loadUXFix();loadDeveloperUX();loadOfferWorkspace();scan(document);startMemoryTimer(); + loadDataLayer();loadServerAutomation();loadHotfix();loadOpsUX();loadUXFix();loadDeveloperUX();loadOfferWorkspace();loadAuthSecurity();scan(document);startMemoryTimer(); const mo=new MutationObserver(records=>{records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)queueImageScan(n)}));}); mo.observe(document.documentElement,{childList:true,subtree:true}); document.addEventListener('click',e=>{if(e.target.closest('#sunDeveloperNavV22,[data-dev-tab],#sunDevRefresh'))scheduleMemoryRefresh(100,true)},true); window.addEventListener('sun:cloud-state-applied',()=>scheduleMemoryRefresh(180,true)); document.addEventListener('visibilitychange',()=>{if(!document.hidden&&developerVisible())scheduleMemoryRefresh(50,false)}); - window.SunPerformance={VERSION,scanImages:()=>scan(document),refreshDeveloperMemory:(force=true)=>enhanceDeveloperMemory({force}),loadDataLayer,loadServerAutomation,loadHotfix,loadOpsUX,loadUXFix,loadDeveloperUX,loadOfferWorkspace,disconnect:()=>{mo.disconnect();if(memoryTimer){clearInterval(memoryTimer);memoryTimer=0}if(imageScanTimer){clearTimeout(imageScanTimer);imageScanTimer=0}pendingImageRoots.clear();}}; + window.SunPerformance={VERSION,scanImages:()=>scan(document),refreshDeveloperMemory:(force=true)=>enhanceDeveloperMemory({force}),loadDataLayer,loadServerAutomation,loadHotfix,loadOpsUX,loadUXFix,loadDeveloperUX,loadOfferWorkspace,loadAuthSecurity,disconnect:()=>{mo.disconnect();if(memoryTimer){clearInterval(memoryTimer);memoryTimer=0}if(imageScanTimer){clearTimeout(imageScanTimer);imageScanTimer=0}pendingImageRoots.clear();}}; }; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start(); -})(); +})(); \ No newline at end of file diff --git a/tests/auth-security-v1774.mjs b/tests/auth-security-v1774.mjs new file mode 100644 index 0000000..d8a1247 --- /dev/null +++ b/tests/auth-security-v1774.mjs @@ -0,0 +1,20 @@ +import fs from 'node:fs'; + +const auth=fs.readFileSync('public/core/auth-security-v1774.js','utf8'); +const perf=fs.readFileSync('public/core/performance.js','utf8'); +const migration=fs.readFileSync('ops/sql/SUPABASE-V17.7.4-AUTH-EMAIL-VERIFICATION.sql','utf8'); +let bad=0; +const check=(ok,msg)=>{console.log(`${ok?'OK':'FAIL'}: ${msg}`);if(!ok)bad++;}; + +check(auth.includes('emailRedirectTo:redirectUrl()'),'signup sets explicit confirmation redirect'); +check(auth.includes('showPublicConfirmation')&&auth.includes('Подтвердите email'),'public signup waits for email confirmation'); +check(auth.includes('showInviteConfirmation'),'invite signup waits for email confirmation'); +check(!auth.includes("registration_source:'caterium_public_signup'")&&!auth.includes("registration_source:'caterium_invite_signup'"),'security guard does not trust client-controlled registration_source'); +check(auth.includes("result.data?.session")&&auth.includes('signOutUnsafeSession'),'guard fails closed if server still auto-confirms'); +check(perf.includes('loadAuthSecurity')&&perf.includes('auth-security-v1774.js'),'auth security guard is loaded by performance core'); +check(migration.includes('drop trigger if exists caterium_autoconfirm_signup_v25 on auth.users'),'autoconfirm users trigger is removed'); +check(migration.includes('drop trigger if exists caterium_autoverify_identity_v25 on auth.identities'),'autoverify identity trigger is removed'); +check(migration.includes('drop function if exists public.caterium_autoconfirm_signup_v25()'),'unsafe autoconfirm function is removed'); +check(migration.includes('drop function if exists public.caterium_autoverify_identity_v25()'),'unsafe identity verification function is removed'); + +if(bad)process.exit(1);