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;
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",
diff --git a/public/core/auth-security-v1774.js b/public/core/auth-security-v1774.js
index 96c4fd8..01d1391 100644
--- a/public/core/auth-security-v1774.js
+++ b/public/core/auth-security-v1774.js
@@ -1,192 +1,113 @@
(()=>{
'use strict';
- const VERSION='17.7.3-auth-security-v1';
+ 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 redirectUrl(){
- try{
- const u=new URL(location.href);
- u.hash='';
- return u.toString();
- }catch(_){return location.href.split('#')[0];}
- }
+ 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 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 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}}
+ 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 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.';
+ 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='Не удалось проверить промокод.';}});
}
- if(/rate limit|rate_limit|too many/i.test(message)){
- return 'Слишком много попыток регистрации. Попробуйте немного позже.';
- }
- return message;
}
- async function signOutUnsafeSession(c){
- try{await c?.auth?.signOut?.()}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 showPublicConfirmation(gate,email){
- if(!gate)return;
- gate.innerHTML=`
Подтвердите 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 showPublicConfirmation(gate,email){if(!gate)return;gate.innerHTML=`
Подтвердите email
Регистрация Caterium
Мы отправили письмо на ${esc(email)}. После подтверждения войдите в Caterium — компания и пробный период будут созданы автоматически. Название компании вы укажете в настройках.
`;gate.querySelector('#sunAuthGoLoginV1774')?.addEventListener('click',()=>location.reload(),{once:true});}
+ function showInviteConfirmation(gate,email){if(!gate)return;gate.innerHTML=`
Подтвердите email
Приглашение сохранено
Письмо подтверждения отправлено на ${esc(email)}. После подтверждения войдите по ссылке приглашения — Caterium автоматически подключит вас к компании.
`;gate.querySelector('#sunInviteReloadV1774')?.addEventListener('click',()=>location.reload(),{once:true});}
- function showInviteConfirmation(gate,email){
- if(!gate)return;
- gate.innerHTML=`
Подтвердите 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 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 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;}
}
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||''),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{
- 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);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}}});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){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();
- 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;}
+ 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);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;}
+ }
- busy=true;
- const button=$('sunInviteJoinV27');if(button)button.disabled=true;
- setError(gate,'Проверяю аккаунт…');
+ 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 existing=await c.auth.signInWithPassword({email,password});
- if(!existing.error&&existing.data?.session){
- await acceptInviteAfterLogin(token,gate);
- return;
- }
- const result=await c.auth.signUp({
- email,
- password,
- options:{
- emailRedirectTo:redirectUrl(),
- data:{name}
- }
- });
- if(result.error)throw result.error;
- if(result.data?.session){
- await signOutUnsafeSession(c);
- throw new Error('Защита email ещё не активирована на сервере. Приглашение остановлено, чтобы не создавать неподтверждённый аккаунт.');
- }
- showInviteConfirmation(gate,email);
- }catch(error){
- setError(gate,friendlySignupError(error));
- if(button)button.disabled=false;
- }finally{busy=false;}
+ const 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;
+ }catch(error){setError(gate,friendlySignupError(error));return false;}finally{busy=false;}
}
- function publicRegistrationMode(){
- const fields=$('sunGateRegisterFieldsV27');
- return Boolean(fields&&!fields.hidden);
+ 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());
}
- 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);
+ 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')){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);
- 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 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
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='