246 lines
10 KiB
PL/PgSQL
246 lines
10 KiB
PL/PgSQL
-- 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;
|