-- Caterium v17.5.27: clear registration + email-bound employee invitations alter table public.sun_workspace_invites add column if not exists email text; alter table public.sun_workspace_invites add column if not exists display_name text; create index if not exists sun_workspace_invites_pending_email_v27_idx on public.sun_workspace_invites(workspace_id, lower(email)) where used_at is null; -- Public Caterium signups and invite signups do not require clicking an email-confirmation link. -- This keeps the existing Auth signup endpoint/rate limits while marking these app-originated users verified. create or replace function public.caterium_autoconfirm_signup_v25() returns trigger language plpgsql security definer set search_path = 'auth','public' as $$ begin if coalesce(new.raw_user_meta_data->>'registration_source','') in ('caterium_public_signup','caterium_invite_signup') then new.email_confirmed_at := coalesce(new.email_confirmed_at, now()); new.raw_user_meta_data := jsonb_set(coalesce(new.raw_user_meta_data,'{}'::jsonb),'{email_verified}','true'::jsonb,true); end if; return new; end; $$; create or replace function public.caterium_autoverify_identity_v25() returns trigger language plpgsql security definer set search_path = 'auth','public' as $$ begin if new.provider='email' and exists ( select 1 from auth.users u where u.id=new.user_id and coalesce(u.raw_user_meta_data->>'registration_source','') in ('caterium_public_signup','caterium_invite_signup') ) then new.identity_data := jsonb_set(coalesce(new.identity_data,'{}'::jsonb),'{email_verified}','true'::jsonb,true); end if; return new; 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 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 not public.sun_has_permission(p_workspace,'users.manage') then raise exception 'Недостаточно прав для добавления пользователей'; end if; if v_role not in ('admin','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; -- Remove stale invites and replace the previous pending invite for the same email. 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),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.sun_has_permission(p_workspace,'users.manage') 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.sun_has_permission(p_workspace,'users.manage') 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; $$; -- Token is the secret. This preview intentionally reveals only the invited company/name/email/role. create or replace function public.sun_invite_preview_v27(p_token uuid) returns table( workspace_id uuid, workspace_name text, email text, display_name text, role text, expires_at timestamptz, is_valid boolean ) language sql stable security definer set search_path='public' as $$ select w.id,w.name,i.email,i.display_name,i.role,i.expires_at, (i.used_at is null and i.expires_at>now()) as is_valid from public.sun_workspace_invites i join public.sun_workspaces w on w.id=i.workspace_id where i.token=p_token limit 1 $$; -- Keep the existing RPC name for backward compatibility, but make new email-bound invites safe. create or replace function public.sun_accept_invite(p_token uuid) returns uuid language plpgsql security definer set search_path='public','auth' as $$ declare v_user uuid := auth.uid(); v_invite public.sun_workspace_invites%rowtype; v_display text; v_user_email text; v_max integer; v_count integer; begin if v_user is null then raise exception 'Сначала войдите в Caterium'; end if; select * into v_invite from public.sun_workspace_invites where token=p_token for update; if not found then raise exception 'Приглашение не найдено'; end if; if v_invite.used_at is not null then raise exception 'Приглашение уже использовано'; end if; if v_invite.expires_at < now() then raise exception 'Срок действия приглашения истёк'; end if; select lower(coalesce(email,'')), coalesce(nullif(v_invite.display_name,''),nullif(raw_user_meta_data->>'name',''),split_part(coalesce(email,'Сотрудник'),'@',1)) into v_user_email,v_display from auth.users where id=v_user; if nullif(lower(trim(coalesce(v_invite.email,''))),'') is not null and lower(trim(v_invite.email))<>v_user_email then raise exception 'Это приглашение создано для другого email'; end if; if public.sun_subscription_access_mode(v_invite.workspace_id)<>'full' then raise exception 'Подписка компании неактивна'; end if; if not public.sun_workspace_has_feature(v_invite.workspace_id,'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=v_invite.workspace_id; select count(*)::int into v_count from public.sun_workspace_members where workspace_id=v_invite.workspace_id and is_active=true; if not exists(select 1 from public.sun_workspace_members where workspace_id=v_invite.workspace_id and user_id=v_user and is_active=true) then if v_max is not null and v_count>=v_max then raise exception 'Достигнут лимит пользователей тарифа (%)',v_max; end if; end if; insert into public.sun_workspace_members(workspace_id,user_id,role,display_name,is_active,permissions,updated_at) values(v_invite.workspace_id,v_user,v_invite.role,v_display,true,coalesce(v_invite.permissions,public.sun_role_default_permissions(v_invite.role)),now()) on conflict(workspace_id,user_id) do update set role=excluded.role, display_name=coalesce(nullif(public.sun_workspace_members.display_name,''),excluded.display_name), is_active=true, permissions=excluded.permissions, updated_at=now(); update public.sun_workspace_invites set used_by=v_user,used_at=now() where token=p_token; return v_invite.workspace_id; end; $$; 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; revoke all on function public.sun_invite_preview_v27(uuid) from public; revoke all on function public.sun_accept_invite(uuid) from public,anon; 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; grant execute on function public.sun_invite_preview_v27(uuid) to anon,authenticated; grant execute on function public.sun_accept_invite(uuid) to authenticated;