Security: require verified email for signup

* security: require verified email for client signups

* security: load verified-email auth guard

* test: include auth security guard in deploy checks

* test: guard verified-email signup flow

* security: remove client-controlled email autoconfirm triggers
This commit is contained in:
pavlov346346-source 2026-09-11 07:27:37 +03:00 committed by GitHub
parent 606572eb87
commit 736e551369
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 233 additions and 5 deletions

View File

@ -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;

View File

@ -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",

View File

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

View File

@ -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();
})();
})();

View File

@ -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);