Merge production (account center + login redesign) into main
production had diverged from main with 20 unreviewed direct-push commits never merged back (account center feature, owner-only employee roles, and a login-screen redesign - the exact "cream login" work that replaced the old dark table-photo screen). Neither QA nor the audit fixes on main had ever seen this code. Conflict resolution: - service-worker.js: kept production's newer cache-refresh mechanism (CRITICAL_FRESH, forceFresh, withAccountCenter, v81 cache name) and combined both sides' CORE asset lists (account-center-v1780.js + login-signature-v1776.js from production, auth-security-v1774.js + order-enhancements-v1775.js from main). - deploy-timeweb.yml: kept main's version, which already independently verifies service-worker.js's sha256 alongside the login/logo files - strictly more thorough than production's version of the same check. Also fixes fallout from production's commits never having been QA-tested before landing: package.json was bumped to 17.8.0 with nothing else in the codebase updated to match (reverted to 17.7.3, matching package-lock.json/release-manifest.json/app-runtime.js, since no other release artifact actually changed), and three tests (static-security.mjs, edge-security-v1774.mjs, release-check.mjs) had hardcoded strings (old PWA cache name, old employee role list) that no longer matched the code they were checking. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
12ba761af9
@ -1 +1 @@
|
||||
First Cloudflare autodeploy
|
||||
Timeweb verification trigger 2026-09-12 08:15
|
||||
|
||||
471
ops/sql/CATERIUM-OWNER-ACCOUNTS-V31.sql
Normal file
471
ops/sql/CATERIUM-OWNER-ACCOUNTS-V31.sql
Normal file
@ -0,0 +1,471 @@
|
||||
-- Caterium v31: one company owner account, owner-only employee administration,
|
||||
-- and a self-service profile for every authenticated user.
|
||||
|
||||
create or replace function public.caterium_is_workspace_owner(p_workspace uuid)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path='public'
|
||||
as $$
|
||||
select auth.uid() is not null
|
||||
and exists(
|
||||
select 1
|
||||
from public.sun_workspaces w
|
||||
join public.sun_workspace_members m
|
||||
on m.workspace_id=w.id
|
||||
and m.user_id=auth.uid()
|
||||
and m.is_active=true
|
||||
where w.id=p_workspace
|
||||
and w.created_by=auth.uid()
|
||||
);
|
||||
$$;
|
||||
|
||||
revoke all on function public.caterium_is_workspace_owner(uuid) from public,anon;
|
||||
grant execute on function public.caterium_is_workspace_owner(uuid) to authenticated;
|
||||
|
||||
create or replace function public.caterium_account_snapshot(p_workspace uuid)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
stable
|
||||
security definer
|
||||
set search_path='public','auth'
|
||||
as $$
|
||||
declare
|
||||
v_user uuid:=auth.uid();
|
||||
v_row record;
|
||||
begin
|
||||
if v_user is null then raise exception 'Сначала войдите в Caterium'; end if;
|
||||
|
||||
select w.id as workspace_id,
|
||||
w.name as company_name,
|
||||
w.company_number,
|
||||
w.created_by,
|
||||
m.display_name,
|
||||
m.role,
|
||||
m.is_active,
|
||||
coalesce(m.permissions,'{}'::jsonb) as permissions,
|
||||
u.email::text as email
|
||||
into v_row
|
||||
from public.sun_workspaces w
|
||||
join public.sun_workspace_members m
|
||||
on m.workspace_id=w.id
|
||||
and m.user_id=v_user
|
||||
join auth.users u on u.id=v_user
|
||||
where w.id=p_workspace
|
||||
and m.is_active=true
|
||||
limit 1;
|
||||
|
||||
if not found then raise exception 'Нет доступа к компании'; end if;
|
||||
|
||||
return jsonb_build_object(
|
||||
'user_id',v_user,
|
||||
'email',v_row.email,
|
||||
'display_name',coalesce(nullif(v_row.display_name,''),split_part(coalesce(v_row.email,''),'@',1)),
|
||||
'role',v_row.role,
|
||||
'is_owner',(v_row.created_by=v_user),
|
||||
'workspace_id',v_row.workspace_id,
|
||||
'company_name',v_row.company_name,
|
||||
'company_number',v_row.company_number,
|
||||
'permissions',v_row.permissions
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.caterium_account_snapshot(uuid) from public,anon;
|
||||
grant execute on function public.caterium_account_snapshot(uuid) to authenticated;
|
||||
|
||||
create or replace function public.caterium_update_my_name(p_display_name text)
|
||||
returns text
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path='public','auth'
|
||||
as $$
|
||||
declare
|
||||
v_user uuid:=auth.uid();
|
||||
v_name text:=trim(coalesce(p_display_name,''));
|
||||
begin
|
||||
if v_user is null then raise exception 'Сначала войдите в Caterium'; end if;
|
||||
if char_length(v_name)<1 then raise exception 'Введите имя'; end if;
|
||||
if char_length(v_name)>120 then raise exception 'Имя слишком длинное'; end if;
|
||||
|
||||
update public.sun_workspace_members
|
||||
set display_name=v_name,
|
||||
updated_at=now()
|
||||
where user_id=v_user;
|
||||
|
||||
update auth.users
|
||||
set raw_user_meta_data=jsonb_set(coalesce(raw_user_meta_data,'{}'::jsonb),'{name}',to_jsonb(v_name),true)
|
||||
where id=v_user;
|
||||
|
||||
return v_name;
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.caterium_update_my_name(text) from public,anon;
|
||||
grant execute on function public.caterium_update_my_name(text) to authenticated;
|
||||
|
||||
-- Existing company creators are the only company administrators.
|
||||
update public.sun_workspace_members m
|
||||
set role='admin',
|
||||
permissions=jsonb_set(coalesce(m.permissions,public.sun_role_default_permissions('admin')),'{users.manage}','true'::jsonb,true),
|
||||
updated_at=now()
|
||||
from public.sun_workspaces w
|
||||
where w.id=m.workspace_id
|
||||
and m.user_id=w.created_by;
|
||||
|
||||
-- Any non-owner account is an employee account and can never manage users.
|
||||
update public.sun_workspace_members m
|
||||
set role=case when m.role='admin' then 'manager' else m.role end,
|
||||
permissions=jsonb_set(coalesce(m.permissions,public.sun_role_default_permissions(case when m.role='admin' then 'manager' else m.role end)),'{users.manage}','false'::jsonb,true),
|
||||
updated_at=now()
|
||||
from public.sun_workspaces w
|
||||
where w.id=m.workspace_id
|
||||
and m.user_id<>w.created_by;
|
||||
|
||||
-- No pending invitation may create a second company administrator.
|
||||
delete from public.sun_workspace_invites where used_at is null and role='admin';
|
||||
|
||||
create or replace function public.sun_list_workspace_members(p_workspace uuid)
|
||||
returns table(user_id uuid,email text,display_name text,role text,is_active boolean,permissions jsonb,created_at timestamptz,updated_at timestamptz)
|
||||
language plpgsql
|
||||
stable
|
||||
security definer
|
||||
set search_path='public','auth'
|
||||
as $$
|
||||
begin
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then
|
||||
raise exception 'Только владелец компании может управлять пользователями';
|
||||
end if;
|
||||
return query
|
||||
select m.user_id,u.email::text,m.display_name,m.role,m.is_active,
|
||||
coalesce(m.permissions,public.sun_role_default_permissions(m.role)),m.created_at,m.updated_at
|
||||
from public.sun_workspace_members m
|
||||
join auth.users u on u.id=m.user_id
|
||||
join public.sun_workspaces w on w.id=m.workspace_id
|
||||
where m.workspace_id=p_workspace
|
||||
order by (m.user_id=w.created_by) desc,coalesce(m.display_name,u.email::text);
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sun_admin_update_member(
|
||||
p_workspace uuid,
|
||||
p_user uuid,
|
||||
p_display_name text,
|
||||
p_role text,
|
||||
p_is_active boolean,
|
||||
p_permissions jsonb
|
||||
)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path='public'
|
||||
as $$
|
||||
declare
|
||||
v_role text:=lower(coalesce(p_role,''));
|
||||
v_owner uuid;
|
||||
v_max integer;
|
||||
v_active_count integer;
|
||||
v_permissions jsonb;
|
||||
begin
|
||||
perform pg_advisory_xact_lock(hashtext(p_workspace::text));
|
||||
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then
|
||||
raise exception 'Только владелец компании может управлять сотрудниками';
|
||||
end if;
|
||||
if public.sun_subscription_access_mode(p_workspace)<>'full' then raise exception 'Подписка не позволяет изменять пользователей'; end if;
|
||||
if not public.sun_workspace_has_feature(p_workspace,'users_manage') then raise exception 'Управление сотрудниками недоступно на текущем тарифе'; end if;
|
||||
if v_role not in ('manager','kitchen','courier','viewer') then raise exception 'Сотруднику нельзя назначить роль владельца'; end if;
|
||||
if p_permissions is null or jsonb_typeof(p_permissions)<>'object' then raise exception 'Некорректные права пользователя'; end if;
|
||||
|
||||
select created_by into v_owner from public.sun_workspaces where id=p_workspace;
|
||||
if v_owner is null then raise exception 'Компания не найдена'; end if;
|
||||
if p_user=v_owner then raise exception 'Главный аккаунт компании нельзя изменить через управление сотрудниками'; end if;
|
||||
if not exists(select 1 from public.sun_workspace_members where workspace_id=p_workspace and user_id=p_user) then raise exception 'Пользователь не найден'; end if;
|
||||
|
||||
if coalesce(p_is_active,false)
|
||||
and not coalesce((select is_active from public.sun_workspace_members where workspace_id=p_workspace and user_id=p_user),false) then
|
||||
select p.max_members into v_max
|
||||
from public.sun_workspace_subscriptions s
|
||||
join public.sun_plans p on p.id=s.plan_id
|
||||
where s.workspace_id=p_workspace;
|
||||
select count(*)::int into v_active_count
|
||||
from public.sun_workspace_members
|
||||
where workspace_id=p_workspace and is_active=true;
|
||||
if v_max is not null and v_active_count>=v_max then raise exception 'Достигнут лимит сотрудников тарифа (%).',v_max; end if;
|
||||
end if;
|
||||
|
||||
v_permissions:=coalesce(p_permissions,'{}'::jsonb)||jsonb_build_object('users.manage',false);
|
||||
|
||||
update public.sun_workspace_members
|
||||
set display_name=nullif(trim(coalesce(p_display_name,'')),''),
|
||||
role=v_role,
|
||||
is_active=coalesce(p_is_active,false),
|
||||
permissions=v_permissions,
|
||||
updated_at=now()
|
||||
where workspace_id=p_workspace and user_id=p_user;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sun_admin_remove_member(p_workspace uuid,p_user uuid)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path='public'
|
||||
as $$
|
||||
declare
|
||||
v_owner uuid;
|
||||
begin
|
||||
perform pg_advisory_xact_lock(hashtext(p_workspace::text));
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then
|
||||
raise exception 'Только владелец компании может удалять сотрудников';
|
||||
end if;
|
||||
if public.sun_subscription_access_mode(p_workspace)<>'full' then raise exception 'Подписка не позволяет изменять пользователей'; end if;
|
||||
if not public.sun_workspace_has_feature(p_workspace,'users_manage') then raise exception 'Управление сотрудниками недоступно на текущем тарифе'; end if;
|
||||
select created_by into v_owner from public.sun_workspaces where id=p_workspace;
|
||||
if p_user=v_owner then raise exception 'Главный аккаунт компании удалить нельзя'; end if;
|
||||
delete from public.sun_workspace_members where workspace_id=p_workspace and user_id=p_user;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sun_employee_prepare_v28(
|
||||
p_workspace uuid,
|
||||
p_email text,
|
||||
p_display_name text default '',
|
||||
p_role text default 'manager'
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path='public','auth'
|
||||
as $$
|
||||
declare
|
||||
v_email text:=lower(trim(coalesce(p_email,'')));
|
||||
v_name text:=trim(coalesce(p_display_name,''));
|
||||
v_role text:=lower(coalesce(p_role,'manager'));
|
||||
v_user uuid;
|
||||
v_member public.sun_workspace_members%rowtype;
|
||||
v_max integer;
|
||||
v_count integer;
|
||||
v_mode text;
|
||||
begin
|
||||
if auth.uid() is null then raise exception 'Сначала войдите в Caterium'; end if;
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then raise exception 'Только владелец компании может добавлять сотрудников'; end if;
|
||||
if v_email='' or v_email !~* '^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$' then raise exception 'Введите корректный email сотрудника'; end if;
|
||||
if v_role not in ('manager','kitchen','courier','viewer') then raise exception 'Сотруднику нельзя назначить роль владельца'; end if;
|
||||
if v_name='' then v_name:=split_part(v_email,'@',1); end if;
|
||||
|
||||
v_mode:=public.sun_workspace_access_mode_internal_v28(p_workspace);
|
||||
if v_mode<>'full' then raise exception 'Подписка компании не позволяет добавлять сотрудников'; end if;
|
||||
if not public.sun_workspace_feature_internal_v28(p_workspace,'users_manage') then raise exception 'Добавление сотрудников недоступно на текущем тарифе'; end if;
|
||||
|
||||
select p.max_members into v_max
|
||||
from public.sun_workspace_subscriptions s join public.sun_plans p on p.id=s.plan_id
|
||||
where s.workspace_id=p_workspace;
|
||||
|
||||
select count(*)::int into v_count from public.sun_workspace_members
|
||||
where workspace_id=p_workspace and is_active=true;
|
||||
|
||||
select u.id into v_user from auth.users u where lower(coalesce(u.email,''))=v_email order by u.created_at asc limit 1;
|
||||
|
||||
if v_user is not null then
|
||||
select * into v_member from public.sun_workspace_members
|
||||
where workspace_id=p_workspace and user_id=v_user limit 1;
|
||||
if found and v_member.is_active then
|
||||
return jsonb_build_object('status','already_member','user_id',v_user,'email',v_email,'display_name',coalesce(nullif(v_member.display_name,''),v_name),'role',v_member.role);
|
||||
end if;
|
||||
end if;
|
||||
|
||||
if v_max is not null and v_count>=v_max then raise exception 'Достигнут лимит пользователей тарифа (%)',v_max; end if;
|
||||
|
||||
return jsonb_build_object(
|
||||
'status',case when v_user is null then 'new' else 'existing' end,
|
||||
'user_id',v_user,
|
||||
'email',v_email,
|
||||
'display_name',v_name,
|
||||
'role',v_role,
|
||||
'max_members',v_max,
|
||||
'active_members',v_count
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sun_employee_finalize_v28(
|
||||
p_workspace uuid,
|
||||
p_user_id uuid,
|
||||
p_display_name text default '',
|
||||
p_role text default 'manager'
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path='public','auth'
|
||||
as $$
|
||||
declare
|
||||
v_name text:=trim(coalesce(p_display_name,''));
|
||||
v_role text:=lower(coalesce(p_role,'manager'));
|
||||
v_email text;
|
||||
v_max integer;
|
||||
v_count integer;
|
||||
v_already boolean:=false;
|
||||
begin
|
||||
if auth.uid() is null then raise exception 'Сначала войдите в Caterium'; end if;
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then raise exception 'Только владелец компании может добавлять сотрудников'; end if;
|
||||
if public.sun_workspace_access_mode_internal_v28(p_workspace)<>'full' then raise exception 'Подписка компании не позволяет добавлять сотрудников'; end if;
|
||||
if not public.sun_workspace_feature_internal_v28(p_workspace,'users_manage') then raise exception 'Добавление сотрудников недоступно на текущем тарифе'; end if;
|
||||
if v_role not in ('manager','kitchen','courier','viewer') then raise exception 'Сотруднику нельзя назначить роль владельца'; end if;
|
||||
|
||||
select lower(email) into v_email from auth.users where id=p_user_id;
|
||||
if v_email is null then raise exception 'Аккаунт сотрудника не найден'; end if;
|
||||
if v_name='' then v_name:=split_part(v_email,'@',1); end if;
|
||||
|
||||
select exists(select 1 from public.sun_workspace_members where workspace_id=p_workspace and user_id=p_user_id and is_active=true) into v_already;
|
||||
select p.max_members into v_max
|
||||
from public.sun_workspace_subscriptions s join public.sun_plans p on p.id=s.plan_id
|
||||
where s.workspace_id=p_workspace;
|
||||
select count(*)::int into v_count from public.sun_workspace_members where workspace_id=p_workspace and is_active=true;
|
||||
if not v_already and v_max is not null and v_count>=v_max then raise exception 'Достигнут лимит пользователей тарифа (%)',v_max; end if;
|
||||
|
||||
insert into public.sun_workspace_members(workspace_id,user_id,role,display_name,is_active,permissions,updated_at)
|
||||
values(p_workspace,p_user_id,v_role,left(v_name,120),true,public.sun_role_default_permissions(v_role)||jsonb_build_object('users.manage',false),now())
|
||||
on conflict(workspace_id,user_id) do update
|
||||
set role=excluded.role,display_name=excluded.display_name,is_active=true,permissions=excluded.permissions,updated_at=now();
|
||||
|
||||
delete from public.sun_workspace_invites where workspace_id=p_workspace and used_at is null and lower(coalesce(email,''))=v_email;
|
||||
|
||||
return jsonb_build_object('status',case when v_already then 'updated' else 'added' end,'user_id',p_user_id,'email',v_email,'display_name',v_name,'role',v_role);
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sun_create_invite(p_workspace uuid,p_role text default 'manager')
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path='public'
|
||||
as $$
|
||||
declare
|
||||
v_token uuid;
|
||||
v_role text:=lower(coalesce(p_role,'manager'));
|
||||
v_max integer;
|
||||
v_count integer;
|
||||
begin
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then raise exception 'Только владелец компании может добавлять сотрудников'; end if;
|
||||
if public.sun_subscription_access_mode(p_workspace)<>'full' then raise exception 'Подписка не позволяет изменять пользователей'; end if;
|
||||
if not public.sun_workspace_has_feature(p_workspace,'users_manage') then raise exception 'Добавление сотрудников недоступно на текущем тарифе'; end if;
|
||||
if v_role not in ('manager','kitchen','courier','viewer') then raise exception 'Некорректная роль сотрудника'; end if;
|
||||
select p.max_members into v_max from public.sun_workspace_subscriptions s join public.sun_plans p on p.id=s.plan_id where s.workspace_id=p_workspace;
|
||||
select count(*)::int into v_count from public.sun_workspace_members where workspace_id=p_workspace and is_active=true;
|
||||
if v_max is not null and v_count>=v_max then raise exception 'Достигнут лимит сотрудников тарифа (%).',v_max; end if;
|
||||
insert into public.sun_workspace_invites(workspace_id,role,permissions,created_by)
|
||||
values(p_workspace,v_role,public.sun_role_default_permissions(v_role)||jsonb_build_object('users.manage',false),auth.uid()) returning token into v_token;
|
||||
return v_token;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sun_create_invite_v27(
|
||||
p_workspace uuid,
|
||||
p_email text,
|
||||
p_display_name text default '',
|
||||
p_role text default 'manager'
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path='public','auth'
|
||||
as $$
|
||||
declare
|
||||
v_token uuid;
|
||||
v_role text:=lower(coalesce(p_role,'manager'));
|
||||
v_email text:=lower(trim(coalesce(p_email,'')));
|
||||
v_name text:=trim(coalesce(p_display_name,''));
|
||||
v_max integer;
|
||||
v_active integer;
|
||||
v_pending integer;
|
||||
begin
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then raise exception 'Только владелец компании может добавлять сотрудников'; end if;
|
||||
if public.sun_subscription_access_mode(p_workspace)<>'full' then raise exception 'Подписка не позволяет изменять пользователей'; end if;
|
||||
if not public.sun_workspace_has_feature(p_workspace,'users_manage') then raise exception 'Добавление сотрудников недоступно на текущем тарифе'; end if;
|
||||
if v_role not in ('manager','kitchen','courier','viewer') then raise exception 'Некорректная роль сотрудника'; end if;
|
||||
if v_email='' or v_email !~* '^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$' then raise exception 'Введите корректный email сотрудника'; end if;
|
||||
if v_name='' then v_name:=split_part(v_email,'@',1); end if;
|
||||
|
||||
if exists(
|
||||
select 1 from public.sun_workspace_members m
|
||||
join auth.users u on u.id=m.user_id
|
||||
where m.workspace_id=p_workspace and m.is_active=true and lower(coalesce(u.email,''))=v_email
|
||||
) then raise exception 'Пользователь с этим email уже добавлен в компанию'; end if;
|
||||
|
||||
delete from public.sun_workspace_invites
|
||||
where workspace_id=p_workspace and used_at is null
|
||||
and (expires_at<=now() or lower(coalesce(email,''))=v_email);
|
||||
|
||||
select p.max_members into v_max
|
||||
from public.sun_workspace_subscriptions s
|
||||
join public.sun_plans p on p.id=s.plan_id
|
||||
where s.workspace_id=p_workspace;
|
||||
|
||||
if v_max is not null then
|
||||
select count(*)::int into v_active from public.sun_workspace_members where workspace_id=p_workspace and is_active=true;
|
||||
select count(*)::int into v_pending from public.sun_workspace_invites where workspace_id=p_workspace and used_at is null and expires_at>now();
|
||||
if v_active+v_pending>=v_max then raise exception 'Достигнут лимит пользователей тарифа (%)',v_max; end if;
|
||||
end if;
|
||||
|
||||
insert into public.sun_workspace_invites(workspace_id,role,permissions,created_by,email,display_name)
|
||||
values(p_workspace,v_role,public.sun_role_default_permissions(v_role)||jsonb_build_object('users.manage',false),auth.uid(),v_email,left(v_name,120))
|
||||
returning token into v_token;
|
||||
return v_token;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sun_list_workspace_invites_v27(p_workspace uuid)
|
||||
returns table(token uuid,email text,display_name text,role text,created_at timestamptz,expires_at timestamptz,status text)
|
||||
language plpgsql
|
||||
stable
|
||||
security definer
|
||||
set search_path='public'
|
||||
as $$
|
||||
begin
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then raise exception 'Только владелец компании может просматривать приглашения'; end if;
|
||||
return query
|
||||
select i.token,i.email,i.display_name,i.role,i.created_at,i.expires_at,
|
||||
case when i.expires_at<=now() then 'expired' else 'pending' end::text
|
||||
from public.sun_workspace_invites i
|
||||
where i.workspace_id=p_workspace and i.used_at is null
|
||||
order by i.created_at desc;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sun_cancel_invite_v27(p_workspace uuid,p_token uuid)
|
||||
returns boolean
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path='public'
|
||||
as $$
|
||||
declare v_deleted integer;
|
||||
begin
|
||||
if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then raise exception 'Только владелец компании может отменять приглашения'; end if;
|
||||
delete from public.sun_workspace_invites where workspace_id=p_workspace and token=p_token and used_at is null;
|
||||
get diagnostics v_deleted=row_count;
|
||||
return v_deleted>0;
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.sun_list_workspace_members(uuid) from public,anon;
|
||||
revoke all on function public.sun_admin_update_member(uuid,uuid,text,text,boolean,jsonb) from public,anon;
|
||||
revoke all on function public.sun_admin_remove_member(uuid,uuid) from public,anon;
|
||||
revoke all on function public.sun_employee_prepare_v28(uuid,text,text,text) from public,anon;
|
||||
revoke all on function public.sun_employee_finalize_v28(uuid,uuid,text,text) from public,anon;
|
||||
revoke all on function public.sun_create_invite(uuid,text) from public,anon;
|
||||
revoke all on function public.sun_create_invite_v27(uuid,text,text,text) from public,anon;
|
||||
revoke all on function public.sun_list_workspace_invites_v27(uuid) from public,anon;
|
||||
revoke all on function public.sun_cancel_invite_v27(uuid,uuid) from public,anon;
|
||||
|
||||
grant execute on function public.sun_list_workspace_members(uuid) to authenticated;
|
||||
grant execute on function public.sun_admin_update_member(uuid,uuid,text,text,boolean,jsonb) to authenticated;
|
||||
grant execute on function public.sun_admin_remove_member(uuid,uuid) to authenticated;
|
||||
grant execute on function public.sun_employee_prepare_v28(uuid,text,text,text) to authenticated;
|
||||
grant execute on function public.sun_employee_finalize_v28(uuid,uuid,text,text) to authenticated;
|
||||
grant execute on function public.sun_create_invite(uuid,text) to authenticated;
|
||||
grant execute on function public.sun_create_invite_v27(uuid,text,text,text) to authenticated;
|
||||
grant execute on function public.sun_list_workspace_invites_v27(uuid) to authenticated;
|
||||
grant execute on function public.sun_cancel_invite_v27(uuid,uuid) to authenticated;
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "caterium-app",
|
||||
"private": true,
|
||||
"version": "17.7.3",
|
||||
"version": "17.8.0",
|
||||
"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/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/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",
|
||||
|
||||
19
public/.htaccess
Normal file
19
public/.htaccess
Normal file
@ -0,0 +1,19 @@
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "^(index\.html|service-worker\.js)$">
|
||||
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "^(performance\.js|login-signature-v1776\.js|auth-security-v1774\.js)$">
|
||||
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/html "access plus 0 seconds"
|
||||
ExpiresByType application/javascript "access plus 0 seconds"
|
||||
</IfModule>
|
||||
1
public/caterium-mark-light.svg
Normal file
1
public/caterium-mark-light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
151
public/core/account-center-v1780.js
Normal file
151
public/core/account-center-v1780.js
Normal file
@ -0,0 +1,151 @@
|
||||
(()=>{
|
||||
'use strict';
|
||||
if(window.CateriumAccountCenterV1780)return;
|
||||
const VERSION='17.8.0-account-center-v3';
|
||||
const $=(s,r=document)=>r.querySelector(s);
|
||||
const qa=(s,r=document)=>[...r.querySelectorAll(s)];
|
||||
const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'');
|
||||
const cloud=()=>window.SunCloudV2||null;
|
||||
const client=()=>cloud()?.getClient?.()||null;
|
||||
const workspace=()=>cloud()?.getWorkspace?.()||null;
|
||||
const session=()=>cloud()?.getSession?.()||null;
|
||||
const toast=(text,type='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(text,type,ms)}catch(_){console.log('[AccountCenter]',text)}};
|
||||
const ROLE_LABELS={admin:'Владелец',manager:'Менеджер',kitchen:'Кухня',courier:'Курьер',viewer:'Просмотр'};
|
||||
let snapshot=null;
|
||||
let modal=null;
|
||||
let lastUserId='';
|
||||
|
||||
function installStyle(){
|
||||
if($('#caterium-account-center-style'))return;
|
||||
const s=document.createElement('style');s.id='caterium-account-center-style';s.textContent=`
|
||||
#cateriumAccountCenter{position:fixed;inset:0;z-index:21000;display:none;place-items:center;padding:22px;background:#102c3d99;backdrop-filter:blur(5px)}
|
||||
#cateriumAccountCenter.on{display:grid}
|
||||
#cateriumAccountCenter .cac-shell{width:min(980px,100%);max-height:min(860px,92vh);overflow:auto;background:#f6f2ea;border-radius:22px;box-shadow:0 28px 90px #0004;color:#2f2b25}
|
||||
#cateriumAccountCenter .cac-head{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;padding:28px 30px 20px;border-bottom:1px solid #d9d1c5}
|
||||
#cateriumAccountCenter h2{margin:0;font:500 38px/1.04 Georgia,'Times New Roman',serif;letter-spacing:-.03em;color:#27231f}
|
||||
#cateriumAccountCenter .cac-sub{margin-top:7px;color:#8a8379;font-size:14px}
|
||||
#cateriumAccountCenter .cac-close{border:0;background:transparent;font-size:31px;color:#5c564d;padding:0 4px}
|
||||
#cateriumAccountCenter .cac-body{padding:24px 30px 30px}
|
||||
#cateriumAccountCenter .cac-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||
#cateriumAccountCenter .cac-card{background:#fffaf2;border:1px solid #ddd4c7;border-radius:16px;padding:20px}
|
||||
#cateriumAccountCenter .cac-card.wide{grid-column:1/-1}
|
||||
#cateriumAccountCenter .cac-card h3{margin:0 0 14px;color:#302b25;font-size:17px}
|
||||
#cateriumAccountCenter .cac-meta{display:grid;gap:8px;margin:0 0 16px}.cac-meta div{display:flex;justify-content:space-between;gap:20px;padding-bottom:8px;border-bottom:1px dashed #e3dbcf}.cac-meta span{color:#8c857b}.cac-meta b{color:#332f29;text-align:right}
|
||||
#cateriumAccountCenter label{display:flex;flex-direction:column;gap:6px;margin:11px 0;color:#777066;font-size:12px;font-weight:700}
|
||||
#cateriumAccountCenter input{min-height:48px;border:1px solid #cfc6ba;border-radius:11px;background:#fff;padding:0 13px;color:#2e2a25;font-size:15px;outline:none}
|
||||
#cateriumAccountCenter input:focus{border-color:#d0a13c;box-shadow:0 0 0 4px rgba(208,161,60,.11)}
|
||||
#cateriumAccountCenter .cac-actions{display:flex;gap:9px;flex-wrap:wrap;margin-top:14px}
|
||||
#cateriumAccountCenter button.primary{border:0;border-radius:11px;background:linear-gradient(100deg,#d9aa42,#efca69);color:#171512;padding:12px 16px;font-weight:900}
|
||||
#cateriumAccountCenter button.outline{border:1px solid #c9c1b6;border-radius:11px;background:#fffaf2;color:#37332d;padding:11px 15px;font-weight:800}
|
||||
#cateriumAccountCenter .cac-note{font-size:12px;color:#8a8379;line-height:1.55;margin-top:9px}
|
||||
#cateriumAccountCenter .cac-owner{display:flex;align-items:center;gap:10px;padding:11px 13px;background:#fbf2d9;border:1px solid #ead39a;border-radius:12px;margin-bottom:12px}.cac-owner b{display:block;color:#725317}.cac-owner span{color:#8a6e36;font-size:12px}
|
||||
#cateriumAccountCenter .cac-company-actions{display:flex;gap:10px;flex-wrap:wrap}
|
||||
#sunCurrentUserLabel{cursor:pointer}
|
||||
#sunCurrentUserLabel:hover{text-decoration:underline;text-underline-offset:3px}
|
||||
#cateriumAccountButton{display:flex!important}
|
||||
#sun-cloud-users-modal [data-caterium-owner-row="1"]{background:#fff9e8;border:1px solid #ead39a;border-radius:10px;padding:10px}
|
||||
#sun-cloud-users-modal .caterium-owner-chip{display:inline-flex;align-items:center;border-radius:999px;background:#f4dfad;color:#6d5118;padding:5px 9px;font-size:11px;font-weight:900}
|
||||
@media(max-width:720px){#cateriumAccountCenter{padding:0;place-items:stretch}#cateriumAccountCenter .cac-shell{width:100%;max-height:none;height:100dvh;border-radius:0}#cateriumAccountCenter .cac-grid{grid-template-columns:1fr}#cateriumAccountCenter .cac-card.wide{grid-column:auto}#cateriumAccountCenter .cac-head,#cateriumAccountCenter .cac-body{padding-left:18px;padding-right:18px}}
|
||||
`;document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function ensureModal(){
|
||||
if(modal)return modal;
|
||||
modal=document.createElement('div');modal.id='cateriumAccountCenter';modal.innerHTML='<div class="cac-shell"><div class="cac-head"><div><h2>Мой аккаунт</h2><div class="cac-sub">Профиль и безопасность Caterium</div></div><button class="cac-close" type="button" aria-label="Закрыть">×</button></div><div class="cac-body" id="cateriumAccountCenterBody"></div></div>';
|
||||
document.body.appendChild(modal);
|
||||
modal.addEventListener('click',e=>{if(e.target===modal||e.target.closest('.cac-close'))close();});
|
||||
return modal;
|
||||
}
|
||||
async function loadSnapshot(){
|
||||
const c=client(),ws=workspace();if(!c||!ws?.id)return null;
|
||||
const {data,error}=await c.rpc('caterium_account_snapshot',{p_workspace:ws.id});
|
||||
if(error)throw error;snapshot=data||null;return snapshot;
|
||||
}
|
||||
function render(){
|
||||
const body=$('#cateriumAccountCenterBody');if(!body)return;
|
||||
const ss=session(),s=snapshot||{},owner=Boolean(s.is_owner);
|
||||
const companyNo=s.company_number?`№${s.company_number}`:'—';
|
||||
body.innerHTML=`<div class="cac-grid">
|
||||
<section class="cac-card">
|
||||
<h3>Профиль</h3>
|
||||
${owner?'<div class="cac-owner"><div><b>Главный аккаунт компании</b><span>Только этот аккаунт создаёт сотрудников и распределяет права.</span></div></div>':''}
|
||||
<div class="cac-meta"><div><span>Компания</span><b>${esc(s.company_name||'—')} · ${esc(companyNo)}</b></div><div><span>Статус</span><b>${esc(owner?'Владелец':ROLE_LABELS[s.role]||s.role||'Сотрудник')}</b></div></div>
|
||||
<label>Имя<input id="cacName" value="${esc(s.display_name||ss?.user?.user_metadata?.name||'')}"></label>
|
||||
<div class="cac-actions"><button class="primary" id="cacSaveName" type="button">Сохранить имя</button></div>
|
||||
</section>
|
||||
<section class="cac-card">
|
||||
<h3>Email для входа</h3>
|
||||
<label>Текущий email<input value="${esc(ss?.user?.email||s.email||'')}" disabled></label>
|
||||
<label>Новый email<input id="cacEmail" type="email" autocomplete="email" placeholder="new@email.com"></label>
|
||||
<div class="cac-actions"><button class="primary" id="cacSaveEmail" type="button">Изменить email</button></div>
|
||||
<div class="cac-note">На новый адрес придёт письмо подтверждения.</div>
|
||||
</section>
|
||||
<section class="cac-card">
|
||||
<h3>Смена пароля</h3>
|
||||
<label>Новый пароль<input id="cacPassword" type="password" autocomplete="new-password" minlength="8"></label>
|
||||
<label>Повторите пароль<input id="cacPassword2" type="password" autocomplete="new-password" minlength="8"></label>
|
||||
<div class="cac-actions"><button class="primary" id="cacSavePassword" type="button">Сменить пароль</button></div>
|
||||
<div class="cac-note">Пароль меняется только для вашего аккаунта. Администратор компании его не видит.</div>
|
||||
</section>
|
||||
${owner?`<section class="cac-card"><h3>Компания и пользователи</h3><p class="cac-note">Создавайте сотрудников, назначайте им рабочую роль и включайте только нужные права.</p><div class="cac-company-actions"><button class="primary" id="cacOpenUsers" type="button">Пользователи и права</button></div></section>`:''}
|
||||
<section class="cac-card wide"><h3>Сеанс</h3><div class="cac-actions"><button class="outline" id="cacLogout" type="button">Выйти из аккаунта</button></div></section>
|
||||
</div>`;
|
||||
$('#cacSaveName').onclick=saveName;$('#cacSaveEmail').onclick=saveEmail;$('#cacSavePassword').onclick=savePassword;$('#cacLogout').onclick=logout;
|
||||
const users=$('#cacOpenUsers');if(users)users.onclick=()=>{close();window.SunAdminRBACV3?.openUsers?.();setTimeout(sanitizeRbac,100);};
|
||||
}
|
||||
async function saveName(){
|
||||
const name=String($('#cacName')?.value||'').trim();if(!name)return toast('Введите имя.','warn');
|
||||
const c=client();if(!c)return;const btn=$('#cacSaveName');btn.disabled=true;
|
||||
try{const {data,error}=await c.rpc('caterium_update_my_name',{p_display_name:name});if(error)throw error;const r=await c.auth.updateUser({data:{name:data||name}});if(r.error)throw r.error;await cloud()?.reloadMemberships?.();await loadSnapshot();render();window.SunAdminRBACV3?.apply?.();toast('Имя изменено.','success');}
|
||||
catch(e){toast(e?.message||String(e),'error',6500)}finally{btn.disabled=false}
|
||||
}
|
||||
async function saveEmail(){
|
||||
const email=String($('#cacEmail')?.value||'').trim().toLowerCase();if(!email)return toast('Введите новый email.','warn');
|
||||
const c=client();if(!c)return;const btn=$('#cacSaveEmail');btn.disabled=true;
|
||||
try{const {error}=await c.auth.updateUser({email});if(error)throw error;$('#cacEmail').value='';toast('На новый email отправлено письмо подтверждения.','success',6500)}catch(e){toast(e?.message||String(e),'error',6500)}finally{btn.disabled=false}
|
||||
}
|
||||
async function savePassword(){
|
||||
const p=String($('#cacPassword')?.value||''),p2=String($('#cacPassword2')?.value||'');
|
||||
if(p.length<8)return toast('Пароль должен быть не короче 8 символов.','warn');if(p!==p2)return toast('Пароли не совпадают.','warn');
|
||||
const c=client();if(!c)return;const btn=$('#cacSavePassword');btn.disabled=true;
|
||||
try{const {error}=await c.auth.updateUser({password:p});if(error)throw error;$('#cacPassword').value='';$('#cacPassword2').value='';toast('Пароль изменён.','success')}
|
||||
catch(e){toast(e?.message||String(e),'error',6500)}finally{btn.disabled=false}
|
||||
}
|
||||
async function logout(){try{await cloud()?.signOut?.()}catch(_){try{await client()?.auth.signOut()}catch(__){}location.reload()}}
|
||||
async function open(){
|
||||
ensureModal();modal.classList.add('on');const body=$('#cateriumAccountCenterBody');body.innerHTML='<p class="cac-note">Загружаю аккаунт…</p>';
|
||||
try{await loadSnapshot();render()}catch(e){body.innerHTML=`<p class="cac-note">${esc(e?.message||e||'Не удалось загрузить аккаунт.')}</p>`}
|
||||
}
|
||||
function close(){modal?.classList.remove('on')}
|
||||
function sanitizeRbac(){
|
||||
const body=$('#sunRbacBody');if(!body)return;
|
||||
qa('select option[value="admin"]',body).forEach(o=>o.remove());
|
||||
qa('[data-rbac-perm="users.manage"]',body).forEach(i=>{i.checked=false;i.disabled=true;const row=i.closest('label');if(row)row.title='Это право принадлежит только главному аккаунту компании';});
|
||||
if(snapshot?.is_owner&&snapshot?.user_id){
|
||||
const btn=body.querySelector(`[data-rbac-edit="${CSS.escape(String(snapshot.user_id))}"]`);
|
||||
if(btn){const row=btn.closest('.sun-rbac-member');if(row){row.dataset.cateriumOwnerRow='1';const role=row.querySelector('.role-chip');if(role)role.textContent='Владелец';btn.outerHTML='<span class="caterium-owner-chip">Главный аккаунт</span>';}}
|
||||
}
|
||||
const addRole=$('#sunRbacEmployeeRole',body);if(addRole)qa('option[value="admin"]',addRole).forEach(o=>o.remove());
|
||||
}
|
||||
function ensureSidebarEntry(){
|
||||
const footer=$('.sun-side-footer');if(!footer||!session()?.user)return;
|
||||
let button=$('#cateriumAccountButton');
|
||||
if(!button){
|
||||
button=document.createElement('button');
|
||||
button.id='cateriumAccountButton';button.type='button';button.className='sun-side-action';button.title='Профиль, email и пароль';
|
||||
button.innerHTML='<span>◎</span><span>Мой аккаунт</span>';
|
||||
footer.insertBefore(button,footer.querySelector('#sunLogoutBtn')||footer.firstChild);
|
||||
}
|
||||
if(!button.dataset.cateriumAccountBound){button.dataset.cateriumAccountBound='1';button.addEventListener('click',open)}
|
||||
}
|
||||
function bindEntry(){
|
||||
const label=$('#sunCurrentUserLabel');if(label&&!label.dataset.cateriumAccountBound){label.dataset.cateriumAccountBound='1';label.title='Открыть личный кабинет';label.addEventListener('click',open)}
|
||||
const ss=session();const uid=ss?.user?.id||'';if(uid&&uid!==lastUserId){lastUserId=uid;snapshot=null;}
|
||||
ensureSidebarEntry();sanitizeRbac();
|
||||
}
|
||||
installStyle();ensureModal();bindEntry();
|
||||
const obs=new MutationObserver(bindEntry);obs.observe(document.documentElement,{childList:true,subtree:true});
|
||||
window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(bindEntry,0));
|
||||
setInterval(()=>{if(!document.hidden)bindEntry()},3000);
|
||||
window.CateriumAccountCenterV1780=Object.freeze({VERSION,open,close,reload:async()=>{await loadSnapshot();render();return snapshot}});
|
||||
})();
|
||||
@ -1,112 +1,30 @@
|
||||
(()=>{
|
||||
'use strict';
|
||||
if(window.CateriumLoginSignatureV1776)return;
|
||||
const VERSION='17.7.6-signature-login-v2';
|
||||
const VERSION='17.7.7-cream-login-v2';
|
||||
const $=(s,r=document)=>r.querySelector(s);
|
||||
const boot=()=>$('#cateriumAuthBootV1776');
|
||||
const removeBoot=()=>{const b=boot();if(b){b.classList.add('leave');setTimeout(()=>b.remove(),180)}};
|
||||
|
||||
function installStyle(){
|
||||
if($('#caterium-login-signature-v1776-style'))return;
|
||||
const style=document.createElement('style');
|
||||
style.id='caterium-login-signature-v1776-style';
|
||||
style.textContent=`
|
||||
body.sun-cloud-auth-required{overflow:hidden!important;background:#091019!important}
|
||||
body.sun-cloud-auth-required>header,body.sun-cloud-auth-required>.view{visibility:hidden!important}
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate{
|
||||
position:fixed!important;inset:0!important;z-index:20000!important;overflow:hidden!important;
|
||||
display:grid!important;grid-template-columns:minmax(520px,47%) 1fr!important;place-items:stretch!important;
|
||||
padding:0!important;background:#091019!important;color:#fff!important;
|
||||
}
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate::before{
|
||||
content:'';position:absolute;z-index:0;inset:0 47% 0 0;pointer-events:none;
|
||||
background:radial-gradient(circle at 74% 48%,rgba(226,169,59,.13),transparent 18%),linear-gradient(125deg,#091019 0%,#111922 62%,#17191b 100%);
|
||||
}
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate::after{
|
||||
content:'';position:absolute;z-index:0;top:0;right:0;bottom:0;width:53%;pointer-events:none;
|
||||
background-image:linear-gradient(90deg,#111922 0%,rgba(17,25,34,.52) 12%,rgba(0,0,0,.12) 44%,rgba(0,0,0,.08) 100%),url('offer-gallery/002.jpg');
|
||||
background-size:cover;background-position:center;filter:saturate(.92) contrast(1.04);
|
||||
}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card{
|
||||
grid-column:1!important;align-self:center!important;justify-self:center!important;position:relative!important;z-index:3!important;
|
||||
width:min(470px,calc(100% - 72px))!important;margin:0!important;padding:24px 0 34px!important;
|
||||
border:0!important;border-radius:0!important;background:transparent!important;color:#f7f4ee!important;
|
||||
box-shadow:none!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important;
|
||||
}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card::before{
|
||||
content:'C';position:fixed;z-index:-1;left:-7vw;top:50%;transform:translateY(-53%);pointer-events:none;
|
||||
font:400 min(68vw,790px)/.72 Georgia,'Times New Roman',serif;color:transparent;
|
||||
-webkit-text-stroke:min(5.8vw,82px) rgba(255,255,255,.09);text-stroke:min(5.8vw,82px) rgba(255,255,255,.09);
|
||||
}
|
||||
#sunCloudAuthGateV3 .caterium-signature-brand{display:flex;align-items:center;gap:12px;margin:0 0 42px;color:#fff}
|
||||
#sunCloudAuthGateV3 .caterium-signature-c{position:relative;display:inline-grid;place-items:center;width:52px;height:52px;font:500 58px/1 Georgia,'Times New Roman',serif;letter-spacing:-8px;color:#fff}
|
||||
#sunCloudAuthGateV3 .caterium-signature-c i{position:absolute;right:-2px;top:23px;width:9px;height:9px;border-radius:50%;background:#e3aa3f;box-shadow:0 0 20px rgba(227,170,63,.3)}
|
||||
#sunCloudAuthGateV3 .caterium-signature-word{font:500 30px/1 Georgia,'Times New Roman',serif;color:#fff}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-brand{display:block!important;margin:0 0 27px!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-brand img{display:none!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-brand h2{margin:0!important;max-width:440px;color:#fff!important;font:500 clamp(37px,3.25vw,52px)/1.04 Georgia,'Times New Roman',serif!important;letter-spacing:-.025em!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-brand .hint{margin-top:10px!important;color:rgba(255,255,255,.67)!important;font-size:15px!important;line-height:1.5!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card>p.hint{color:rgba(255,255,255,.68)!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card label{display:block!important;margin:14px 0 0!important;color:rgba(255,255,255,.72)!important;font-size:12px!important;font-weight:700!important;letter-spacing:.015em!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card input,#sunCloudAuthGateV3 .sun-cloud-auth-card select,#sunCloudAuthGateV3 .sun-cloud-auth-card textarea{
|
||||
width:100%!important;height:48px!important;margin-top:6px!important;padding:0 15px!important;border:1px solid rgba(255,255,255,.27)!important;border-radius:9px!important;
|
||||
background:rgba(8,12,16,.36)!important;color:#fff!important;box-shadow:none!important;outline:none!important;
|
||||
}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card input:focus{border-color:#e3aa3f!important;box-shadow:0 0 0 3px rgba(227,170,63,.12)!important;background:rgba(8,12,16,.58)!important}
|
||||
#sunCloudAuthGateV3 .sun-auth-eye{right:7px!important;bottom:7px!important;color:rgba(255,255,255,.78)!important;background:transparent!important}
|
||||
#sunCloudAuthGateV3 .sun-auth-eye:hover{background:rgba(255,255,255,.08)!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-actions{display:block!important;margin-top:20px!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-actions .primary,#sunCloudAuthGateV3 #sunGateSubmitV3{
|
||||
width:100%!important;min-height:50px!important;border:0!important;border-radius:9px!important;background:linear-gradient(100deg,#dba137,#efc45f)!important;
|
||||
color:#111!important;font-size:15px!important;font-weight:900!important;box-shadow:0 13px 30px rgba(207,145,37,.17)!important;
|
||||
}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-actions .primary:hover,#sunCloudAuthGateV3 #sunGateSubmitV3:hover{filter:brightness(1.05)!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-actions .outline{width:100%!important;margin-top:8px!important;border-color:rgba(255,255,255,.22)!important;background:rgba(255,255,255,.04)!important;color:#fff!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-error{min-height:18px!important;margin-top:8px!important;color:#ffb6ad!important;font-size:12px!important}
|
||||
#sunCloudAuthGateV3 .sun-auth-switch{margin:20px 0 0!important;padding-top:19px!important;border-top:1px solid rgba(255,255,255,.16)!important;color:rgba(255,255,255,.7)!important;font-size:13px!important}
|
||||
#sunCloudAuthGateV3 .sun-auth-switch button{border:0!important;background:transparent!important;color:#e8b34c!important;text-decoration:none!important;font-weight:900!important}
|
||||
#sunCloudAuthGateV3 .caterium-signature-caption{position:fixed;left:42px;bottom:32px;z-index:4;color:rgba(255,255,255,.6);font-size:9px;letter-spacing:.3em;text-transform:uppercase;pointer-events:none}
|
||||
#sunCloudAuthGateV3 .caterium-signature-caption::before{content:'';display:block;width:45px;height:2px;margin-bottom:12px;background:#e3aa3f}
|
||||
#sunCloudAuthGateV3 .caterium-signature-manifesto{position:fixed;right:42px;top:36px;z-index:4;width:150px;color:rgba(255,255,255,.7);font-size:9px;line-height:1.9;letter-spacing:.23em;text-transform:uppercase;pointer-events:none;text-shadow:0 1px 10px #0008}
|
||||
#sunCloudAuthGateV3 .caterium-signature-manifesto::after{content:'';display:block;width:42px;height:2px;margin-top:12px;background:#e3aa3f}
|
||||
@media(max-width:900px){
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate{display:grid!important;grid-template-columns:1fr!important;background:#091019!important}
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate::before{inset:0;background:linear-gradient(180deg,rgba(7,13,20,.72),rgba(7,13,20,.9))}
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate::after{width:100%;opacity:.55;background-image:linear-gradient(180deg,rgba(5,10,15,.38),rgba(5,10,15,.88)),url('offer-gallery/002.jpg');background-position:center}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card{grid-column:1!important;width:min(460px,calc(100% - 36px))!important;padding:20px 20px 24px!important;border:1px solid rgba(255,255,255,.12)!important;border-radius:18px!important;background:rgba(7,12,17,.66)!important;backdrop-filter:blur(13px)!important;-webkit-backdrop-filter:blur(13px)!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card::before{left:-52vw;top:50%;font-size:120vw;-webkit-text-stroke:14vw rgba(255,255,255,.07)}
|
||||
#sunCloudAuthGateV3 .caterium-signature-brand{margin-bottom:28px}
|
||||
#sunCloudAuthGateV3 .caterium-signature-word{font-size:25px}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-brand h2{font-size:32px!important}
|
||||
#sunCloudAuthGateV3 .caterium-signature-caption,#sunCloudAuthGateV3 .caterium-signature-manifesto{display:none!important}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function decorate(gate){
|
||||
if(!gate)return false;
|
||||
const card=$('.sun-cloud-auth-card',gate);if(!card)return false;
|
||||
gate.classList.add('caterium-signature-ready');
|
||||
if(!$('.caterium-signature-brand',card)){
|
||||
const brand=document.createElement('div');brand.className='caterium-signature-brand';brand.setAttribute('aria-label','Caterium');
|
||||
brand.innerHTML='<span class="caterium-signature-c">C<i></i></span><span class="caterium-signature-word">Caterium</span>';card.prepend(brand);
|
||||
}
|
||||
if(!$('.caterium-signature-caption',gate)){const n=document.createElement('div');n.className='caterium-signature-caption';n.textContent='Caterium · управление кейтерингом';gate.appendChild(n)}
|
||||
if(!$('.caterium-signature-manifesto',gate)){const n=document.createElement('div');n.className='caterium-signature-manifesto';n.textContent='Создаём впечатления, которые объединяют';gate.appendChild(n)}
|
||||
const title=$('#sunGateTitleV3',gate),subtitle=$('#sunGateSubtitleV3',gate);
|
||||
if(title&&title.textContent.trim()==='Вход в Caterium')title.textContent='Войти в рабочее пространство';
|
||||
if(subtitle&&subtitle.textContent.trim()==='Введите данные своего аккаунта')subtitle.textContent='Ваши заказы. Ваша команда. Ваш результат.';
|
||||
removeBoot();return true;
|
||||
}
|
||||
|
||||
function scan(){return decorate($('#sunCloudAuthGateV3'))}
|
||||
function releaseForSignedIn(){
|
||||
try{const st=window.SunCloudV2?.status?.();if(st?.signedIn&&window.SunCloudV2?.getWorkspace?.()&&!$('#sunCloudAuthGateV3'))removeBoot()}catch(_){}
|
||||
}
|
||||
installStyle();scan();
|
||||
const observer=new MutationObserver(()=>{scan();releaseForSignedIn()});observer.observe(document.documentElement,{childList:true,subtree:true,characterData:true});
|
||||
window.addEventListener('sun:cloud-state-applied',()=>{scan();releaseForSignedIn()});
|
||||
const poll=setInterval(()=>{if(scan()||!boot()){clearInterval(poll);return}releaseForSignedIn()},80);setTimeout(()=>{clearInterval(poll);if(boot()&&!$('#sunCloudAuthGateV3'))removeBoot()},10000);
|
||||
window.CateriumLoginSignatureV1776=Object.freeze({VERSION,scan,removeBoot,disconnect:()=>{observer.disconnect();clearInterval(poll)}});
|
||||
const FLOW='<svg viewBox="0 0 430 220" aria-hidden="true"><path d="M18 42H122V108H220V58H318V164H420"/><circle cx="18" cy="42" r="7"/><circle cx="122" cy="42" r="7"/><circle cx="122" cy="108" r="7"/><circle cx="220" cy="108" r="7"/><circle cx="220" cy="58" r="7"/><circle cx="318" cy="58" r="7"/><circle cx="318" cy="164" r="7"/><circle cx="420" cy="164" r="8" class="gold"/></svg>';
|
||||
function styles(){if($('#caterium-login-signature-v1776-style'))return;const s=document.createElement('style');s.id='caterium-login-signature-v1776-style';s.textContent=`
|
||||
body.sun-cloud-auth-required{overflow:hidden!important;background:#f5f0e7!important}body.sun-cloud-auth-required>header,body.sun-cloud-auth-required>.view{visibility:hidden!important}
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate{position:fixed!important;inset:0!important;z-index:20000!important;overflow:auto!important;display:grid!important;place-items:center!important;padding:clamp(32px,6vh,78px) clamp(24px,8vw,120px)!important;background:radial-gradient(circle at 18% 18%,rgba(255,255,255,.92),transparent 34%),radial-gradient(circle at 77% 22%,rgba(255,255,255,.5),transparent 28%),linear-gradient(135deg,#f8f4ec 0%,#f2ece1 56%,#f7f3eb 100%)!important;color:#282621!important}
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate:before{content:'C';position:fixed;z-index:0;left:-7vw;top:49%;transform:translateY(-50%);pointer-events:none;font:400 min(72vw,860px)/.72 Georgia,'Times New Roman',serif;color:rgba(73,67,59,.032)}
|
||||
#sunCloudAuthGateV3.sun-cloud-auth-gate:after{content:'';position:fixed;z-index:0;right:-7vw;bottom:-14vh;width:min(48vw,720px);height:min(70vh,850px);pointer-events:none;background:radial-gradient(ellipse at 58% 20%,rgba(67,64,58,.14) 0 7%,transparent 8%),radial-gradient(ellipse at 36% 35%,rgba(67,64,58,.13) 0 6%,transparent 7%),radial-gradient(ellipse at 68% 47%,rgba(67,64,58,.12) 0 8%,transparent 9%),linear-gradient(106deg,transparent 43%,rgba(67,64,58,.08) 44% 46%,transparent 47%);filter:blur(18px);transform:rotate(-17deg);opacity:.45}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card{position:relative!important;z-index:3!important;width:min(790px,100%)!important;margin:0!important;padding:0 0 58px!important;border:0!important;border-radius:0!important;background:transparent!important;color:#2c2924!important;box-shadow:none!important;backdrop-filter:none!important}
|
||||
#sunCloudAuthGateV3 .caterium-signature-brand{display:flex;align-items:center;gap:13px;margin:0 0 clamp(46px,7vh,88px)!important}.caterium-signature-mark{display:block;width:62px;height:62px;object-fit:contain;flex:0 0 62px}.caterium-signature-word{font:500 36px/1 Georgia,'Times New Roman',serif;color:#26231f;letter-spacing:-.02em}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-brand{display:block!important;margin:0 0 35px!important}#sunCloudAuthGateV3 .sun-cloud-auth-brand img{display:none!important}#sunCloudAuthGateV3 .sun-cloud-auth-brand h2{margin:0!important;max-width:720px;color:#24211d!important;font:500 clamp(48px,5.2vw,76px)/.99 Georgia,'Times New Roman',serif!important;letter-spacing:-.045em!important}#sunCloudAuthGateV3 .sun-cloud-auth-brand .hint{margin-top:18px!important;color:#8e887f!important;font-size:clamp(16px,1.55vw,21px)!important;line-height:1.45!important;font-weight:400!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card>p.hint{color:#817b72!important}#sunCloudAuthGateV3 .sun-cloud-auth-card label{display:block!important;margin:14px 0 0!important;color:#716b62!important;font-size:12px!important;font-weight:700!important}#sunCloudAuthGateV3 label.caterium-clean-field{font-size:0!important;color:transparent!important;position:relative!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-card input,#sunCloudAuthGateV3 .sun-cloud-auth-card select,#sunCloudAuthGateV3 .sun-cloud-auth-card textarea{width:100%!important;min-height:58px!important;margin-top:7px!important;padding:0 18px!important;border:1px solid #bdb5aa!important;border-radius:13px!important;background:rgba(255,255,255,.13)!important;color:#312e29!important;box-shadow:none!important;outline:none!important;font-size:16px!important;font-weight:500!important}#sunCloudAuthGateV3 label.caterium-clean-field input{margin-top:0!important;padding-left:58px!important}#sunCloudAuthGateV3 input::placeholder{color:#9c958c!important;opacity:1!important}#sunCloudAuthGateV3 input:focus{border-color:#d7a632!important;box-shadow:0 0 0 4px rgba(216,167,50,.12)!important;background:rgba(255,255,255,.5)!important}
|
||||
#sunCloudAuthGateV3 .caterium-email-field:before{content:'✉';position:absolute;z-index:2;left:20px;top:15px;color:#777168;font-size:25px;font-weight:400}#sunCloudAuthGateV3 .caterium-password-field:before{content:'♙';position:absolute;z-index:2;left:21px;top:14px;color:#777168;font-size:24px;transform:rotate(180deg);opacity:.8}
|
||||
#sunCloudAuthGateV3 .sun-auth-password{display:block!important;position:relative!important}#sunCloudAuthGateV3 .sun-auth-password input{padding-right:58px!important}#sunCloudAuthGateV3 .sun-auth-eye{right:9px!important;bottom:10px!important;width:38px!important;height:38px!important;color:#777168!important;background:transparent!important;border-radius:9px!important}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-actions{display:block!important;margin-top:25px!important}#sunCloudAuthGateV3 .sun-cloud-auth-actions .primary,#sunCloudAuthGateV3 #sunGateSubmitV3{width:100%!important;min-height:62px!important;border:0!important;border-radius:13px!important;background:linear-gradient(100deg,#d9aa42,#efca69)!important;color:#171512!important;font-size:18px!important;font-weight:900!important;box-shadow:0 14px 34px rgba(184,135,34,.14)!important}#sunCloudAuthGateV3 #sunGateSubmitV3:after{content:' →';font-size:24px;font-weight:500;margin-left:13px}
|
||||
#sunCloudAuthGateV3 .sun-cloud-auth-actions .outline{width:100%!important;margin-top:9px!important;min-height:48px!important;border:1px solid #c9c1b6!important;border-radius:11px!important;background:rgba(255,255,255,.25)!important;color:#37332d!important}#sunCloudAuthGateV3 .sun-cloud-auth-error{min-height:19px!important;margin-top:10px!important;color:#a64c40!important;font-size:12px!important}#sunCloudAuthGateV3 .sun-auth-switch{margin:26px 0 0!important;padding-top:25px!important;border-top:1px solid #cfc7bc!important;color:#898279!important;font-size:14px!important;text-align:center!important}#sunCloudAuthGateV3 .sun-auth-switch button{border:0!important;background:transparent!important;color:#c18f24!important;text-decoration:none!important;font-weight:900!important}
|
||||
.caterium-signature-caption{position:fixed;left:42px;bottom:31px;z-index:4;color:#9d968d;font-size:9px;letter-spacing:.31em;text-transform:uppercase;pointer-events:none}.caterium-signature-caption:before{content:'';display:block;width:48px;height:2px;margin-bottom:13px;background:#d3a43c}.caterium-signature-manifesto{position:fixed;right:56px;top:44px;z-index:4;width:270px;color:#a19a91;font-size:10px;line-height:1.9;letter-spacing:.28em;text-transform:uppercase;pointer-events:none;white-space:pre-line}.caterium-flow-top{position:fixed;z-index:1;right:5.5vw;top:13vh;width:min(37vw,470px);opacity:.86;pointer-events:none;color:#a7a097}.caterium-flow-top svg{width:100%;height:auto}.caterium-flow-top path{fill:none;stroke:currentColor;stroke-width:1.6}.caterium-flow-top circle{fill:currentColor}.caterium-flow-top circle.gold{fill:#e5a313}
|
||||
@media(max-width:980px){#sunCloudAuthGateV3.sun-cloud-auth-gate{padding:34px 28px 70px!important;place-items:start center!important}#sunCloudAuthGateV3 .sun-cloud-auth-card{width:min(720px,100%)!important;padding-top:12px!important}.caterium-signature-manifesto{display:none!important}.caterium-flow-top{right:-60px;top:125px;width:360px;opacity:.45}}
|
||||
@media(max-width:620px){body.sun-cloud-auth-required{overflow:auto!important}#sunCloudAuthGateV3.sun-cloud-auth-gate{min-height:100dvh!important;padding:25px 18px 50px!important;place-items:start center!important}#sunCloudAuthGateV3 .sun-cloud-auth-card{width:100%!important;padding:0!important}.caterium-signature-brand{margin-bottom:42px!important}.caterium-signature-mark{width:50px;height:50px;flex-basis:50px}.caterium-signature-word{font-size:29px}#sunCloudAuthGateV3 .sun-cloud-auth-brand h2{font-size:clamp(38px,12vw,52px)!important}.caterium-flow-top{right:-125px;top:100px;width:330px;opacity:.25}.caterium-signature-caption{display:none!important}}
|
||||
`;document.head.appendChild(s)}
|
||||
function field(g,id,ph,type){const i=$('#'+id,g);if(!i)return;i.placeholder=ph;const l=i.closest('label');if(l)l.classList.add('caterium-clean-field',type==='email'?'caterium-email-field':'caterium-password-field')}
|
||||
function decorate(g){if(!g)return false;const c=$('.sun-cloud-auth-card',g);if(!c)return false;if(!$('.caterium-signature-brand',c)){const b=document.createElement('div');b.className='caterium-signature-brand';b.innerHTML='<img class="caterium-signature-mark" src="caterium-mark-light.svg" alt=""><span class="caterium-signature-word">Caterium</span>';c.prepend(b)}if(!$('.caterium-signature-caption',g)){const n=document.createElement('div');n.className='caterium-signature-caption';n.textContent='Caterium · вкус в деталях';g.appendChild(n)}if(!$('.caterium-signature-manifesto',g)){const n=document.createElement('div');n.className='caterium-signature-manifesto';n.textContent='Простые решения\nдля больших событий';g.appendChild(n)}if(!$('.caterium-flow-top',g)){const n=document.createElement('div');n.className='caterium-flow-top';n.innerHTML=FLOW;g.appendChild(n)}const t=$('#sunGateTitleV3',g),sub=$('#sunGateSubtitleV3',g);if(t&&t.textContent.trim()==='Вход в Caterium')t.textContent='Войти в рабочее пространство';if(sub&&sub.textContent.trim()==='Введите данные своего аккаунта')sub.textContent='Ваши заказы. Ваша команда. Ваш результат.';field(g,'sunGateEmailV3','Email','email');field(g,'sunGatePasswordV3','Пароль','password');field(g,'sunGatePassword2V27','Подтвердите пароль','password');removeBoot();return true}
|
||||
function scan(){return decorate($('#sunCloudAuthGateV3'))}styles();scan();const o=new MutationObserver(scan);o.observe(document.documentElement,{childList:true,subtree:true,characterData:true});window.addEventListener('sun:cloud-state-applied',scan);const p=setInterval(()=>{if(scan()||!boot())clearInterval(p)},80);setTimeout(()=>{clearInterval(p);if(boot()&&!$('#sunCloudAuthGateV3'))removeBoot()},10000);window.CateriumLoginSignatureV1776=Object.freeze({VERSION,scan,removeBoot,disconnect:()=>{o.disconnect();clearInterval(p)}})
|
||||
})();
|
||||
@ -20,4 +20,14 @@
|
||||
return node;
|
||||
};
|
||||
window.SunSafe=Object.freeze({escapeHTML,escapeAttr,idToken,safeImageSrc,setText,insertBefore});
|
||||
|
||||
// Small bootstrap for account/profile UI. Keeping it here makes the account
|
||||
// center available on every Caterium screen without touching the legacy monolith.
|
||||
if(!document.getElementById('cateriumAccountCenterV1780Script')){
|
||||
const script=document.createElement('script');
|
||||
script.id='cateriumAccountCenterV1780Script';
|
||||
script.src='core/account-center-v1780.js?v=20260912-v17-8-0-account-center-2';
|
||||
script.async=true;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
})();
|
||||
52
public/refresh-login-20260912.html
Normal file
52
public/refresh-login-20260912.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<title>Caterium — обновление</title>
|
||||
<style>
|
||||
html,body{height:100%;margin:0}body{display:grid;place-items:center;background:#f5f0e7;color:#282621;font:16px Arial,sans-serif}.box{width:min(520px,calc(100% - 40px));text-align:center}.mark{width:78px;height:78px;object-fit:contain;margin-bottom:24px}.title{font:500 38px/1.05 Georgia,'Times New Roman',serif;margin:0 0 12px}.text{color:#777168;line-height:1.55}.dot{display:inline-block;width:10px;height:10px;border-radius:50%;background:#e5a313;margin-right:8px;animation:pulse 1s infinite alternate}@keyframes pulse{to{opacity:.25;transform:scale(.8)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="box">
|
||||
<img class="mark" src="/caterium-mark-light.svg?refresh=20260912-3" alt="Caterium">
|
||||
<h1 class="title">Обновляем Caterium</h1>
|
||||
<p class="text" id="status"><span class="dot"></span>Удаляем старый экран входа и подключаем свежую версию…</p>
|
||||
</main>
|
||||
<script>
|
||||
(async()=>{
|
||||
const status=document.getElementById('status');
|
||||
const stamp='20260912-3-'+Date.now();
|
||||
try{
|
||||
if('serviceWorker' in navigator){
|
||||
const regs=await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(regs.map(r=>r.unregister()));
|
||||
}
|
||||
if('caches' in window){
|
||||
const keys=await caches.keys();
|
||||
await Promise.all(keys.map(k=>caches.delete(k)));
|
||||
}
|
||||
if('serviceWorker' in navigator){
|
||||
const reg=await navigator.serviceWorker.register('/service-worker.js?reset='+encodeURIComponent(stamp),{scope:'/'});
|
||||
const worker=reg.installing||reg.waiting||reg.active;
|
||||
if(worker && worker.state!=='activated'){
|
||||
await new Promise(resolve=>{
|
||||
const done=()=>{if(worker.state==='activated'||worker.state==='redundant')resolve()};
|
||||
worker.addEventListener('statechange',done);done();setTimeout(resolve,5000);
|
||||
});
|
||||
}
|
||||
await navigator.serviceWorker.ready.catch(()=>{});
|
||||
}
|
||||
status.innerHTML='<span class="dot"></span>Готово. Открываем свежий экран входа…';
|
||||
setTimeout(()=>location.replace('/?fresh='+encodeURIComponent(stamp)),350);
|
||||
}catch(err){
|
||||
status.textContent='Кэш очищен частично. Сейчас всё равно откроем приложение заново.';
|
||||
setTimeout(()=>location.replace('/?fresh='+encodeURIComponent(stamp)),800);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,13 +1,22 @@
|
||||
const CACHE='sun-catering-pwa-v78-20260909-v17-7-3-clients-server-read';
|
||||
const VERSION='20260909-v17-7-3-clients-server-read';
|
||||
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/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}`,`./core/login-signature-v1776.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','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest',
|
||||
'./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'
|
||||
]);
|
||||
self.addEventListener('install',event=>{
|
||||
event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(CORE)).then(()=>self.skipWaiting()));
|
||||
});
|
||||
@ -18,15 +27,34 @@ 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).then(res=>cachePut(req,res)).catch(()=>caches.match(req).then(hit=>hit||caches.match(fallback||req)));
|
||||
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=`<script src="core/account-center-v1780.js?v=${VERSION}"></script>`;
|
||||
const out=html.includes('</body>')?html.replace('</body>',`${tag}</body>`):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).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;
|
||||
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).then(res=>cachePut(req,res))));
|
||||
event.respondWith(caches.match(req).then(cached=>cached||fetch(req,{cache:'no-store'}).then(res=>cachePut(req,res))));
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ const WORKERS_DEV_ORIGIN = /^https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.workers\.dev
|
||||
const LOCAL_ORIGIN = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d{1,5})?$/i;
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const ALLOWED_ROLES = new Set(["admin", "manager", "kitchen", "courier", "viewer"]);
|
||||
const ALLOWED_ROLES = new Set(["manager", "kitchen", "courier", "viewer"]);
|
||||
|
||||
function allowedOrigin(origin: string) {
|
||||
if (!origin) return true;
|
||||
@ -42,7 +42,7 @@ type PrepData = {
|
||||
|
||||
function publicError(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error || "");
|
||||
if (/access denied|permission|not allowed|недостаточно прав|сначала войдите/i.test(message)) return { status: 403, error: "Недостаточно прав для добавления сотрудника.", code: "forbidden" };
|
||||
if (/access denied|permission|not allowed|недостаточно прав|сначала войдите|только владелец/i.test(message)) return { status: 403, error: "Только владелец компании может добавлять сотрудников.", code: "forbidden" };
|
||||
if (/subscription|blocked|plan|подписк|тариф/i.test(message)) return { status: 403, error: "Добавление сотрудника недоступно для текущего тарифа.", code: "plan_restricted" };
|
||||
if (/limit|maximum|max_members|лимит/i.test(message)) return { status: 409, error: "Достигнут лимит сотрудников для текущего тарифа.", code: "member_limit" };
|
||||
if (/already registered|already exists|duplicate|уже существует/i.test(message)) return { status: 409, error: "Аккаунт с этим email уже существует.", code: "account_exists" };
|
||||
@ -82,7 +82,7 @@ Deno.serve(async (req: Request) => {
|
||||
if (!UUID_RE.test(workspaceId)) return reply(req, { error: "Некорректная компания.", code: "invalid_workspace" }, 400);
|
||||
if (!EMAIL_RE.test(email) || email.length > 254) return reply(req, { error: "Введите корректный email.", code: "invalid_email" }, 400);
|
||||
if (displayName.length > 120) return reply(req, { error: "Имя сотрудника слишком длинное.", code: "invalid_display_name" }, 400);
|
||||
if (!ALLOWED_ROLES.has(role)) return reply(req, { error: "Некорректная роль сотрудника.", code: "invalid_role" }, 400);
|
||||
if (!ALLOWED_ROLES.has(role)) return reply(req, { error: "Для сотрудника выберите рабочую роль: менеджер, кухня, курьер или просмотр.", code: "invalid_role" }, 400);
|
||||
|
||||
const caller = createClient(url, anon, {
|
||||
global: { headers: { Authorization: auth } },
|
||||
|
||||
@ -7,4 +7,6 @@ check(src.includes('return await finalize(existingUserId, false, null)'),'existi
|
||||
check(src.includes('retryStatus === "existing"'),'partial/concurrent auth creation retries finalize');
|
||||
check(src.includes('sun_employee_finalize_v28'),'employee finalize RPC remains required');
|
||||
check(!src.includes('if (prep.data?.status !== "new")'),'old early-return bug is removed');
|
||||
check(src.includes('new Set(["manager", "kitchen", "courier", "viewer"])'),'employee endpoint cannot create another company owner/admin');
|
||||
check(!src.includes('new Set(["admin", "manager"'),'admin role is excluded from employee creation');
|
||||
if(bad)process.exit(1);
|
||||
@ -28,7 +28,7 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
|
||||
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||
if(!sw.includes('v17-7-3-clients-server-read')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.7.0');
|
||||
if(!sw.includes('20260912-login-refresh')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
||||
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user