feat: versión inicial de GLM Hub
This commit is contained in:
@@ -0,0 +1,894 @@
|
||||
-- ============================================================================
|
||||
-- GLM HUB — ESQUEMA COMPLETO DE SUPABASE
|
||||
-- ============================================================================
|
||||
-- Incluye:
|
||||
-- 1) Usuarios autorizados y roles.
|
||||
-- 2) Catálogo compartido para todos los usuarios del Hub.
|
||||
-- 3) Favoritos y accesos recientes por usuario.
|
||||
-- 4) Bucket público para los íconos.
|
||||
-- 5) RLS, permisos y Realtime.
|
||||
-- 6) Función segura que usa n8n para validar al administrador.
|
||||
-- 7) Solicitudes de acceso, aprobadores y decisión única.
|
||||
--
|
||||
-- Ejecutar el archivo completo una sola vez en:
|
||||
-- Supabase Studio > SQL Editor > New query > Run.
|
||||
-- El script es idempotente y puede volver a ejecutarse para restaurar políticas.
|
||||
-- ============================================================================
|
||||
|
||||
begin;
|
||||
|
||||
create extension if not exists pgcrypto with schema extensions;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. USUARIOS AUTORIZADOS
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
create table if not exists public.glm_hub_authorized_users (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
email text not null unique,
|
||||
full_name text,
|
||||
role text not null default 'member',
|
||||
is_active boolean not null default true,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
constraint glm_hub_authorized_users_role_check
|
||||
check (role in ('admin', 'member')),
|
||||
|
||||
constraint glm_hub_authorized_users_email_normalized_check
|
||||
check (email = lower(btrim(email))),
|
||||
|
||||
constraint glm_hub_authorized_users_domain_check
|
||||
check (email ~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$'),
|
||||
|
||||
constraint glm_hub_authorized_users_admin_check
|
||||
check (
|
||||
role <> 'admin'
|
||||
or email in (
|
||||
'jgomez@gomezleemarketing.com',
|
||||
'iaracena@gomezleemarketing.com',
|
||||
'ethen@gomezleemarketing.com',
|
||||
'mgomez@gomezleemarketing.com',
|
||||
'lmatos@gomezleemarketing.com'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
create or replace function public.glm_hub_normalize_authorized_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
new.email := lower(btrim(new.email));
|
||||
new.full_name := nullif(btrim(new.full_name), '');
|
||||
new.updated_at := now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists glm_hub_normalize_authorized_user_trigger
|
||||
on public.glm_hub_authorized_users;
|
||||
|
||||
create trigger glm_hub_normalize_authorized_user_trigger
|
||||
before insert or update on public.glm_hub_authorized_users
|
||||
for each row
|
||||
execute function public.glm_hub_normalize_authorized_user();
|
||||
|
||||
alter table public.glm_hub_authorized_users enable row level security;
|
||||
alter table public.glm_hub_authorized_users force row level security;
|
||||
|
||||
revoke all on table public.glm_hub_authorized_users from anon;
|
||||
revoke all on table public.glm_hub_authorized_users from authenticated;
|
||||
grant select on table public.glm_hub_authorized_users to authenticated;
|
||||
grant all on table public.glm_hub_authorized_users to service_role;
|
||||
|
||||
drop policy if exists "GLM Hub users can read only their active access"
|
||||
on public.glm_hub_authorized_users;
|
||||
|
||||
create policy "GLM Hub users can read only their active access"
|
||||
on public.glm_hub_authorized_users
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
is_active = true
|
||||
and email = lower(coalesce((select auth.jwt() ->> 'email'), ''))
|
||||
);
|
||||
|
||||
insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||
values
|
||||
('jgomez@gomezleemarketing.com', 'José Leopoldo Gómez', 'admin', true),
|
||||
('iaracena@gomezleemarketing.com', 'Isaac Aracena', 'admin', true),
|
||||
('ethen@gomezleemarketing.com', 'Eidan Then', 'admin', true),
|
||||
('mgomez@gomezleemarketing.com', 'Máximo Gomez', 'admin', true),
|
||||
('lmatos@gomezleemarketing.com', 'Luis Matos', 'admin', true)
|
||||
on conflict (email) do update
|
||||
set
|
||||
full_name = excluded.full_name,
|
||||
role = excluded.role,
|
||||
is_active = true,
|
||||
updated_at = now();
|
||||
|
||||
-- Estas funciones se ejecutan con los permisos/RLS del usuario autenticado.
|
||||
create or replace function public.glm_hub_is_active_user()
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security invoker
|
||||
set search_path = public, auth
|
||||
as $$
|
||||
select exists (
|
||||
select 1
|
||||
from public.glm_hub_authorized_users as access
|
||||
where access.is_active = true
|
||||
and access.email = lower(coalesce((select auth.jwt() ->> 'email'), ''))
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace function public.glm_hub_is_admin()
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security invoker
|
||||
set search_path = public, auth
|
||||
as $$
|
||||
select exists (
|
||||
select 1
|
||||
from public.glm_hub_authorized_users as access
|
||||
where access.is_active = true
|
||||
and access.role = 'admin'
|
||||
and access.email = lower(coalesce((select auth.jwt() ->> 'email'), ''))
|
||||
);
|
||||
$$;
|
||||
|
||||
-- n8n llama esta función usando el JWT recibido desde el frontend.
|
||||
-- SECURITY DEFINER permite consultar la tabla interna sin quedar bloqueado por RLS,
|
||||
-- pero el correo y el usuario siempre se toman del JWT real de Supabase.
|
||||
create or replace function public.glm_hub_icon_generation_access()
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
stable
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_email text;
|
||||
v_user_id text;
|
||||
v_authorized boolean;
|
||||
begin
|
||||
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
|
||||
v_user_id := coalesce(auth.uid()::text, '');
|
||||
|
||||
select exists (
|
||||
select 1
|
||||
from public.glm_hub_authorized_users as access
|
||||
where lower(access.email) = v_email
|
||||
and access.role = 'admin'
|
||||
and access.is_active = true
|
||||
)
|
||||
into v_authorized;
|
||||
|
||||
return jsonb_build_object(
|
||||
'authorized', coalesce(v_authorized, false),
|
||||
'email', v_email,
|
||||
'userId', v_user_id
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
alter function public.glm_hub_icon_generation_access() owner to postgres;
|
||||
|
||||
revoke all on function public.glm_hub_is_active_user() from public, anon;
|
||||
revoke all on function public.glm_hub_is_admin() from public, anon;
|
||||
revoke all on function public.glm_hub_icon_generation_access() from public, anon;
|
||||
grant execute on function public.glm_hub_is_active_user() to authenticated;
|
||||
grant execute on function public.glm_hub_is_admin() to authenticated;
|
||||
grant execute on function public.glm_hub_icon_generation_access() to authenticated;
|
||||
grant execute on function public.glm_hub_is_active_user() to service_role;
|
||||
grant execute on function public.glm_hub_is_admin() to service_role;
|
||||
grant execute on function public.glm_hub_icon_generation_access() to service_role;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. CATÁLOGO COMPARTIDO
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
create table if not exists public.glm_hub_apps (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
description text not null,
|
||||
category text not null,
|
||||
url text,
|
||||
icon_url text not null,
|
||||
visibility text not null default 'published',
|
||||
created_by uuid default auth.uid() references auth.users(id) on delete set null,
|
||||
updated_by uuid default auth.uid() references auth.users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
constraint glm_hub_apps_name_length_check
|
||||
check (char_length(btrim(name)) between 2 and 60),
|
||||
constraint glm_hub_apps_description_length_check
|
||||
check (char_length(btrim(description)) between 1 and 180),
|
||||
constraint glm_hub_apps_category_check
|
||||
check (category in ('Administración', 'Recursos Humanos', 'CDC')),
|
||||
constraint glm_hub_apps_visibility_check
|
||||
check (visibility in ('published', 'hidden')),
|
||||
constraint glm_hub_apps_icon_url_check
|
||||
check (icon_url ~ '^https://')
|
||||
);
|
||||
|
||||
create unique index if not exists glm_hub_apps_name_unique_ci
|
||||
on public.glm_hub_apps (lower(btrim(name)));
|
||||
|
||||
create index if not exists glm_hub_apps_visibility_category_name_idx
|
||||
on public.glm_hub_apps (visibility, category, name);
|
||||
|
||||
create or replace function public.glm_hub_normalize_app()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
new.name := btrim(new.name);
|
||||
new.description := btrim(new.description);
|
||||
new.category := btrim(new.category);
|
||||
new.url := nullif(btrim(new.url), '');
|
||||
new.icon_url := btrim(new.icon_url);
|
||||
new.updated_at := now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists glm_hub_normalize_app_trigger on public.glm_hub_apps;
|
||||
create trigger glm_hub_normalize_app_trigger
|
||||
before insert or update on public.glm_hub_apps
|
||||
for each row
|
||||
execute function public.glm_hub_normalize_app();
|
||||
|
||||
alter table public.glm_hub_apps enable row level security;
|
||||
alter table public.glm_hub_apps force row level security;
|
||||
alter table public.glm_hub_apps replica identity full;
|
||||
|
||||
revoke all on table public.glm_hub_apps from anon;
|
||||
revoke all on table public.glm_hub_apps from authenticated;
|
||||
grant select, insert, update, delete on table public.glm_hub_apps to authenticated;
|
||||
grant all on table public.glm_hub_apps to service_role;
|
||||
|
||||
drop policy if exists "GLM Hub active users read shared apps" on public.glm_hub_apps;
|
||||
drop policy if exists "GLM Hub admins create apps" on public.glm_hub_apps;
|
||||
drop policy if exists "GLM Hub admins update apps" on public.glm_hub_apps;
|
||||
drop policy if exists "GLM Hub admins delete apps" on public.glm_hub_apps;
|
||||
|
||||
create policy "GLM Hub active users read shared apps"
|
||||
on public.glm_hub_apps
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
public.glm_hub_is_active_user()
|
||||
and (visibility = 'published' or public.glm_hub_is_admin())
|
||||
);
|
||||
|
||||
create policy "GLM Hub admins create apps"
|
||||
on public.glm_hub_apps
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.glm_hub_is_admin()
|
||||
and created_by = (select auth.uid())
|
||||
and updated_by = (select auth.uid())
|
||||
);
|
||||
|
||||
create policy "GLM Hub admins update apps"
|
||||
on public.glm_hub_apps
|
||||
for update
|
||||
to authenticated
|
||||
using (public.glm_hub_is_admin())
|
||||
with check (
|
||||
public.glm_hub_is_admin()
|
||||
and updated_by = (select auth.uid())
|
||||
);
|
||||
|
||||
create policy "GLM Hub admins delete apps"
|
||||
on public.glm_hub_apps
|
||||
for delete
|
||||
to authenticated
|
||||
using (public.glm_hub_is_admin());
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. FAVORITOS POR USUARIO
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
create table if not exists public.glm_hub_favorites (
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
app_id uuid not null references public.glm_hub_apps(id) on delete cascade,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (user_id, app_id)
|
||||
);
|
||||
|
||||
create index if not exists glm_hub_favorites_user_created_idx
|
||||
on public.glm_hub_favorites (user_id, created_at desc);
|
||||
|
||||
alter table public.glm_hub_favorites enable row level security;
|
||||
alter table public.glm_hub_favorites force row level security;
|
||||
|
||||
revoke all on table public.glm_hub_favorites from anon;
|
||||
revoke all on table public.glm_hub_favorites from authenticated;
|
||||
grant select, insert, update, delete on table public.glm_hub_favorites to authenticated;
|
||||
grant all on table public.glm_hub_favorites to service_role;
|
||||
|
||||
drop policy if exists "GLM Hub users read their favorites" on public.glm_hub_favorites;
|
||||
drop policy if exists "GLM Hub users create their favorites" on public.glm_hub_favorites;
|
||||
drop policy if exists "GLM Hub users update their favorites" on public.glm_hub_favorites;
|
||||
drop policy if exists "GLM Hub users delete their favorites" on public.glm_hub_favorites;
|
||||
|
||||
create policy "GLM Hub users read their favorites"
|
||||
on public.glm_hub_favorites
|
||||
for select
|
||||
to authenticated
|
||||
using (public.glm_hub_is_active_user() and user_id = (select auth.uid()));
|
||||
|
||||
create policy "GLM Hub users create their favorites"
|
||||
on public.glm_hub_favorites
|
||||
for insert
|
||||
to authenticated
|
||||
with check (public.glm_hub_is_active_user() and user_id = (select auth.uid()));
|
||||
|
||||
create policy "GLM Hub users update their favorites"
|
||||
on public.glm_hub_favorites
|
||||
for update
|
||||
to authenticated
|
||||
using (public.glm_hub_is_active_user() and user_id = (select auth.uid()))
|
||||
with check (public.glm_hub_is_active_user() and user_id = (select auth.uid()));
|
||||
|
||||
create policy "GLM Hub users delete their favorites"
|
||||
on public.glm_hub_favorites
|
||||
for delete
|
||||
to authenticated
|
||||
using (public.glm_hub_is_active_user() and user_id = (select auth.uid()));
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. ACCESOS RECIENTES POR USUARIO
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
create table if not exists public.glm_hub_recent_apps (
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
app_id uuid not null references public.glm_hub_apps(id) on delete cascade,
|
||||
last_opened_at timestamptz not null default now(),
|
||||
primary key (user_id, app_id)
|
||||
);
|
||||
|
||||
create index if not exists glm_hub_recent_apps_user_date_idx
|
||||
on public.glm_hub_recent_apps (user_id, last_opened_at desc);
|
||||
|
||||
alter table public.glm_hub_recent_apps enable row level security;
|
||||
alter table public.glm_hub_recent_apps force row level security;
|
||||
|
||||
revoke all on table public.glm_hub_recent_apps from anon;
|
||||
revoke all on table public.glm_hub_recent_apps from authenticated;
|
||||
grant select, insert, update, delete on table public.glm_hub_recent_apps to authenticated;
|
||||
grant all on table public.glm_hub_recent_apps to service_role;
|
||||
|
||||
drop policy if exists "GLM Hub users read their recent apps" on public.glm_hub_recent_apps;
|
||||
drop policy if exists "GLM Hub users create their recent apps" on public.glm_hub_recent_apps;
|
||||
drop policy if exists "GLM Hub users update their recent apps" on public.glm_hub_recent_apps;
|
||||
drop policy if exists "GLM Hub users delete their recent apps" on public.glm_hub_recent_apps;
|
||||
|
||||
create policy "GLM Hub users read their recent apps"
|
||||
on public.glm_hub_recent_apps
|
||||
for select
|
||||
to authenticated
|
||||
using (public.glm_hub_is_active_user() and user_id = (select auth.uid()));
|
||||
|
||||
create policy "GLM Hub users create their recent apps"
|
||||
on public.glm_hub_recent_apps
|
||||
for insert
|
||||
to authenticated
|
||||
with check (public.glm_hub_is_active_user() and user_id = (select auth.uid()));
|
||||
|
||||
create policy "GLM Hub users update their recent apps"
|
||||
on public.glm_hub_recent_apps
|
||||
for update
|
||||
to authenticated
|
||||
using (public.glm_hub_is_active_user() and user_id = (select auth.uid()))
|
||||
with check (public.glm_hub_is_active_user() and user_id = (select auth.uid()));
|
||||
|
||||
create policy "GLM Hub users delete their recent apps"
|
||||
on public.glm_hub_recent_apps
|
||||
for delete
|
||||
to authenticated
|
||||
using (public.glm_hub_is_active_user() and user_id = (select auth.uid()));
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. STORAGE PARA LOS ÍCONOS
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
insert into storage.buckets (
|
||||
id,
|
||||
name,
|
||||
public,
|
||||
file_size_limit,
|
||||
allowed_mime_types
|
||||
)
|
||||
values (
|
||||
'glm-hub-icons',
|
||||
'glm-hub-icons',
|
||||
true,
|
||||
1048576,
|
||||
array['image/png', 'image/jpeg', 'image/webp']::text[]
|
||||
)
|
||||
on conflict (id) do update
|
||||
set
|
||||
name = excluded.name,
|
||||
public = true,
|
||||
file_size_limit = excluded.file_size_limit,
|
||||
allowed_mime_types = excluded.allowed_mime_types;
|
||||
|
||||
drop policy if exists "GLM Hub active users read icons" on storage.objects;
|
||||
drop policy if exists "GLM Hub admins upload icons" on storage.objects;
|
||||
drop policy if exists "GLM Hub admins update icons" on storage.objects;
|
||||
drop policy if exists "GLM Hub admins delete icons" on storage.objects;
|
||||
|
||||
create policy "GLM Hub active users read icons"
|
||||
on storage.objects
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'glm-hub-icons'
|
||||
and public.glm_hub_is_active_user()
|
||||
);
|
||||
|
||||
create policy "GLM Hub admins upload icons"
|
||||
on storage.objects
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'glm-hub-icons'
|
||||
and public.glm_hub_is_admin()
|
||||
);
|
||||
|
||||
create policy "GLM Hub admins update icons"
|
||||
on storage.objects
|
||||
for update
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'glm-hub-icons'
|
||||
and public.glm_hub_is_admin()
|
||||
)
|
||||
with check (
|
||||
bucket_id = 'glm-hub-icons'
|
||||
and public.glm_hub_is_admin()
|
||||
);
|
||||
|
||||
create policy "GLM Hub admins delete icons"
|
||||
on storage.objects
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'glm-hub-icons'
|
||||
and public.glm_hub_is_admin()
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 6. SOLICITUDES DE ACCESO A APLICACIONES
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
create table if not exists public.glm_hub_access_requests (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
requester_user_id uuid not null references auth.users(id) on delete cascade,
|
||||
requester_name text not null,
|
||||
requester_email text not null,
|
||||
app_id uuid references public.glm_hub_apps(id) on delete set null,
|
||||
app_name text not null,
|
||||
app_category text not null,
|
||||
status text not null default 'pending',
|
||||
decided_by_name text,
|
||||
decided_by_email text,
|
||||
decided_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
constraint glm_hub_access_requests_status_check
|
||||
check (status in ('pending', 'approved', 'rejected')),
|
||||
constraint glm_hub_access_requests_requester_email_check
|
||||
check (requester_email = lower(btrim(requester_email))),
|
||||
constraint glm_hub_access_requests_category_check
|
||||
check (app_category in ('Administración', 'Recursos Humanos', 'CDC'))
|
||||
);
|
||||
|
||||
create unique index if not exists glm_hub_access_requests_one_pending_idx
|
||||
on public.glm_hub_access_requests (requester_email, app_id)
|
||||
where status = 'pending' and app_id is not null;
|
||||
|
||||
create index if not exists glm_hub_access_requests_status_created_idx
|
||||
on public.glm_hub_access_requests (status, created_at desc);
|
||||
|
||||
create table if not exists public.glm_hub_access_request_approvers (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
request_id uuid not null references public.glm_hub_access_requests(id) on delete cascade,
|
||||
approver_name text not null,
|
||||
approver_email text not null,
|
||||
decision_token text not null unique,
|
||||
decision_token_hash text not null unique,
|
||||
clicked_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
|
||||
constraint glm_hub_access_request_approvers_email_check
|
||||
check (
|
||||
approver_email in (
|
||||
'iaracena@gomezleemarketing.com',
|
||||
'jgomez@gomezleemarketing.com',
|
||||
'mgomez@gomezleemarketing.com'
|
||||
)
|
||||
),
|
||||
constraint glm_hub_access_request_approvers_unique_request_email
|
||||
unique (request_id, approver_email)
|
||||
);
|
||||
|
||||
alter table public.glm_hub_access_requests enable row level security;
|
||||
alter table public.glm_hub_access_requests force row level security;
|
||||
alter table public.glm_hub_access_request_approvers enable row level security;
|
||||
alter table public.glm_hub_access_request_approvers force row level security;
|
||||
|
||||
revoke all on table public.glm_hub_access_requests from anon, authenticated;
|
||||
revoke all on table public.glm_hub_access_request_approvers from anon, authenticated;
|
||||
grant all on table public.glm_hub_access_requests to service_role;
|
||||
grant all on table public.glm_hub_access_request_approvers to service_role;
|
||||
|
||||
-- Los usuarios pueden consultar solamente sus propias solicitudes desde Supabase,
|
||||
-- aunque el frontend actual no necesita leer esta tabla.
|
||||
drop policy if exists "GLM Hub users read their access requests"
|
||||
on public.glm_hub_access_requests;
|
||||
|
||||
create policy "GLM Hub users read their access requests"
|
||||
on public.glm_hub_access_requests
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
requester_user_id = auth.uid()
|
||||
and public.glm_hub_is_active_user()
|
||||
);
|
||||
|
||||
grant select on table public.glm_hub_access_requests to authenticated;
|
||||
|
||||
-- Registra la solicitud usando exclusivamente la identidad del JWT del usuario.
|
||||
-- No devuelve enlaces de aprobación al navegador, evitando que el solicitante
|
||||
-- pueda obtenerlos o aprobarse a sí mismo.
|
||||
create or replace function public.glm_hub_prepare_access_request(p_app_id uuid)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
volatile
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_user_id uuid;
|
||||
v_email text;
|
||||
v_name text;
|
||||
v_role text;
|
||||
v_app_name text;
|
||||
v_app_category text;
|
||||
v_request_id uuid;
|
||||
begin
|
||||
v_user_id := auth.uid();
|
||||
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
|
||||
|
||||
if v_user_id is null or v_email = '' then
|
||||
return jsonb_build_object('ok', false, 'error', 'Tu sesión no es válida. Cierra sesión e inicia nuevamente.');
|
||||
end if;
|
||||
|
||||
select coalesce(nullif(btrim(access.full_name), ''), split_part(v_email, '@', 1)), access.role
|
||||
into v_name, v_role
|
||||
from public.glm_hub_authorized_users as access
|
||||
where access.email = v_email
|
||||
and access.is_active = true
|
||||
limit 1;
|
||||
|
||||
if not found then
|
||||
return jsonb_build_object('ok', false, 'error', 'El usuario no está autorizado en GLM Hub.');
|
||||
end if;
|
||||
|
||||
if v_role = 'admin' then
|
||||
return jsonb_build_object('ok', false, 'error', 'Los administradores no necesitan solicitar acceso desde el Hub.');
|
||||
end if;
|
||||
|
||||
select app.name, app.category
|
||||
into v_app_name, v_app_category
|
||||
from public.glm_hub_apps as app
|
||||
where app.id = p_app_id
|
||||
and app.visibility = 'published'
|
||||
limit 1;
|
||||
|
||||
if not found then
|
||||
return jsonb_build_object('ok', false, 'error', 'La aplicación seleccionada ya no está publicada.');
|
||||
end if;
|
||||
|
||||
if exists (
|
||||
select 1
|
||||
from public.glm_hub_access_requests as request
|
||||
where request.requester_email = v_email
|
||||
and request.app_id = p_app_id
|
||||
and request.status = 'pending'
|
||||
) then
|
||||
return jsonb_build_object(
|
||||
'ok', false,
|
||||
'code', 'PENDING_EXISTS',
|
||||
'error', 'Ya existe una solicitud pendiente para esta aplicación.'
|
||||
);
|
||||
end if;
|
||||
|
||||
insert into public.glm_hub_access_requests (
|
||||
requester_user_id,
|
||||
requester_name,
|
||||
requester_email,
|
||||
app_id,
|
||||
app_name,
|
||||
app_category
|
||||
)
|
||||
values (
|
||||
v_user_id,
|
||||
v_name,
|
||||
v_email,
|
||||
p_app_id,
|
||||
v_app_name,
|
||||
v_app_category
|
||||
)
|
||||
returning id into v_request_id;
|
||||
|
||||
return jsonb_build_object(
|
||||
'ok', true,
|
||||
'requestId', v_request_id,
|
||||
'requesterName', v_name,
|
||||
'requesterEmail', v_email,
|
||||
'appName', v_app_name,
|
||||
'appCategory', v_app_category
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
alter function public.glm_hub_prepare_access_request(uuid) owner to postgres;
|
||||
revoke all on function public.glm_hub_prepare_access_request(uuid) from public, anon;
|
||||
grant execute on function public.glm_hub_prepare_access_request(uuid) to authenticated;
|
||||
grant execute on function public.glm_hub_prepare_access_request(uuid) to service_role;
|
||||
|
||||
-- Emite o recupera los tres enlaces de decisión. Solo n8n, usando la clave
|
||||
-- service_role guardada en su nodo HTTP Request, puede ejecutar esta función.
|
||||
create or replace function public.glm_hub_issue_access_request_tokens(p_request_id uuid)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
volatile
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_request public.glm_hub_access_requests%rowtype;
|
||||
v_token_isaac text;
|
||||
v_token_jose text;
|
||||
v_token_maximo text;
|
||||
begin
|
||||
select request.*
|
||||
into v_request
|
||||
from public.glm_hub_access_requests as request
|
||||
where request.id = p_request_id
|
||||
and request.status = 'pending'
|
||||
for update;
|
||||
|
||||
if not found then
|
||||
return jsonb_build_object('ok', false, 'error', 'La solicitud no existe o ya fue atendida.');
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1
|
||||
from public.glm_hub_access_request_approvers as approver
|
||||
where approver.request_id = p_request_id
|
||||
) then
|
||||
v_token_isaac := encode(extensions.gen_random_bytes(32), 'hex');
|
||||
v_token_jose := encode(extensions.gen_random_bytes(32), 'hex');
|
||||
v_token_maximo := encode(extensions.gen_random_bytes(32), 'hex');
|
||||
|
||||
insert into public.glm_hub_access_request_approvers (
|
||||
request_id,
|
||||
approver_name,
|
||||
approver_email,
|
||||
decision_token,
|
||||
decision_token_hash
|
||||
)
|
||||
values
|
||||
(
|
||||
p_request_id,
|
||||
'Isaac Aracena',
|
||||
'iaracena@gomezleemarketing.com',
|
||||
v_token_isaac,
|
||||
encode(extensions.digest(v_token_isaac, 'sha256'), 'hex')
|
||||
),
|
||||
(
|
||||
p_request_id,
|
||||
'José Leopoldo Gómez',
|
||||
'jgomez@gomezleemarketing.com',
|
||||
v_token_jose,
|
||||
encode(extensions.digest(v_token_jose, 'sha256'), 'hex')
|
||||
),
|
||||
(
|
||||
p_request_id,
|
||||
'Máximo Gómez',
|
||||
'mgomez@gomezleemarketing.com',
|
||||
v_token_maximo,
|
||||
encode(extensions.digest(v_token_maximo, 'sha256'), 'hex')
|
||||
);
|
||||
end if;
|
||||
|
||||
return jsonb_build_object(
|
||||
'ok', true,
|
||||
'requestId', v_request.id,
|
||||
'requesterName', v_request.requester_name,
|
||||
'requesterEmail', v_request.requester_email,
|
||||
'appName', v_request.app_name,
|
||||
'appCategory', v_request.app_category,
|
||||
'approvers', (
|
||||
select jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', approver.approver_name,
|
||||
'email', approver.approver_email,
|
||||
'token', approver.decision_token
|
||||
)
|
||||
order by approver.approver_email
|
||||
)
|
||||
from public.glm_hub_access_request_approvers as approver
|
||||
where approver.request_id = p_request_id
|
||||
)
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
alter function public.glm_hub_issue_access_request_tokens(uuid) owner to postgres;
|
||||
revoke all on function public.glm_hub_issue_access_request_tokens(uuid) from public, anon, authenticated;
|
||||
grant execute on function public.glm_hub_issue_access_request_tokens(uuid) to service_role;
|
||||
|
||||
-- Registra una sola decisión de forma atómica. Aunque los botones sigan visibles
|
||||
-- en correos ya entregados, cualquier segundo clic queda bloqueado en la base de datos.
|
||||
create or replace function public.glm_hub_decide_access_request(
|
||||
p_token text,
|
||||
p_decision text
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
volatile
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_request_id uuid;
|
||||
v_approver_name text;
|
||||
v_approver_email text;
|
||||
v_request public.glm_hub_access_requests%rowtype;
|
||||
v_normalized_decision text;
|
||||
begin
|
||||
v_normalized_decision := lower(btrim(coalesce(p_decision, '')));
|
||||
|
||||
if v_normalized_decision not in ('approved', 'rejected') then
|
||||
return jsonb_build_object('ok', false, 'error', 'La decisión recibida no es válida.');
|
||||
end if;
|
||||
|
||||
select approver.request_id, approver.approver_name, approver.approver_email
|
||||
into v_request_id, v_approver_name, v_approver_email
|
||||
from public.glm_hub_access_request_approvers as approver
|
||||
where approver.decision_token_hash = encode(
|
||||
extensions.digest(btrim(coalesce(p_token, '')), 'sha256'),
|
||||
'hex'
|
||||
)
|
||||
limit 1;
|
||||
|
||||
if not found then
|
||||
return jsonb_build_object('ok', false, 'error', 'El enlace de decisión no es válido o ya no existe.');
|
||||
end if;
|
||||
|
||||
update public.glm_hub_access_requests as request
|
||||
set
|
||||
status = v_normalized_decision,
|
||||
decided_by_name = v_approver_name,
|
||||
decided_by_email = v_approver_email,
|
||||
decided_at = now(),
|
||||
updated_at = now()
|
||||
where request.id = v_request_id
|
||||
and request.status = 'pending'
|
||||
returning request.* into v_request;
|
||||
|
||||
if found then
|
||||
update public.glm_hub_access_request_approvers
|
||||
set clicked_at = now()
|
||||
where request_id = v_request_id
|
||||
and approver_email = v_approver_email;
|
||||
|
||||
return jsonb_build_object(
|
||||
'ok', true,
|
||||
'alreadyDecided', false,
|
||||
'requestId', v_request.id,
|
||||
'status', v_request.status,
|
||||
'requesterName', v_request.requester_name,
|
||||
'requesterEmail', v_request.requester_email,
|
||||
'appName', v_request.app_name,
|
||||
'appCategory', v_request.app_category,
|
||||
'decidedByName', v_request.decided_by_name,
|
||||
'decidedByEmail', v_request.decided_by_email,
|
||||
'decidedAt', v_request.decided_at
|
||||
);
|
||||
end if;
|
||||
|
||||
select request.*
|
||||
into v_request
|
||||
from public.glm_hub_access_requests as request
|
||||
where request.id = v_request_id;
|
||||
|
||||
return jsonb_build_object(
|
||||
'ok', false,
|
||||
'alreadyDecided', true,
|
||||
'status', v_request.status,
|
||||
'requesterName', v_request.requester_name,
|
||||
'requesterEmail', v_request.requester_email,
|
||||
'appName', v_request.app_name,
|
||||
'decidedByName', coalesce(v_request.decided_by_name, ''),
|
||||
'decidedByEmail', coalesce(v_request.decided_by_email, ''),
|
||||
'decidedAt', v_request.decided_at,
|
||||
'error', 'Esta solicitud ya fue atendida y no admite otra decisión.'
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
alter function public.glm_hub_decide_access_request(text, text) owner to postgres;
|
||||
revoke all on function public.glm_hub_decide_access_request(text, text) from public;
|
||||
grant execute on function public.glm_hub_decide_access_request(text, text) to anon;
|
||||
grant execute on function public.glm_hub_decide_access_request(text, text) to authenticated;
|
||||
grant execute on function public.glm_hub_decide_access_request(text, text) to service_role;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 7. REALTIME DEL CATÁLOGO
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
-- Hace que una aplicación creada/editada/eliminada por un administrador
|
||||
-- aparezca en las sesiones abiertas de los demás usuarios sin recargar.
|
||||
do $$
|
||||
begin
|
||||
if exists (
|
||||
select 1 from pg_publication where pubname = 'supabase_realtime'
|
||||
) and not exists (
|
||||
select 1
|
||||
from pg_publication_tables
|
||||
where pubname = 'supabase_realtime'
|
||||
and schemaname = 'public'
|
||||
and tablename = 'glm_hub_apps'
|
||||
) then
|
||||
execute 'alter publication supabase_realtime add table public.glm_hub_apps';
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
|
||||
commit;
|
||||
|
||||
-- ============================================================================
|
||||
-- OPERACIONES DE ADMINISTRACIÓN (EJECUTAR DESDE SQL EDITOR)
|
||||
-- ============================================================================
|
||||
|
||||
-- DAR ACCESO A UN USUARIO NORMAL:
|
||||
-- insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||
-- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'member', true)
|
||||
-- on conflict (email) do update
|
||||
-- set full_name = excluded.full_name, role = 'member', is_active = true;
|
||||
|
||||
-- QUITAR ACCESO SIN BORRAR EL REGISTRO:
|
||||
-- update public.glm_hub_authorized_users
|
||||
-- set is_active = false
|
||||
-- where email = 'usuario@gomezleemarketing.com';
|
||||
|
||||
-- REACTIVAR ACCESO:
|
||||
-- update public.glm_hub_authorized_users
|
||||
-- set is_active = true
|
||||
-- where email = 'usuario@gomezleemarketing.com';
|
||||
|
||||
-- VER SOLICITUDES DE ACCESO:
|
||||
-- select requester_name, requester_email, app_name, status, decided_by_name, decided_by_email, created_at, decided_at
|
||||
-- from public.glm_hub_access_requests
|
||||
-- order by created_at desc;
|
||||
|
||||
-- VER EL CATÁLOGO COMPARTIDO:
|
||||
-- select id, name, category, visibility, url, icon_url, updated_at
|
||||
-- from public.glm_hub_apps
|
||||
-- order by name;
|
||||
Reference in New Issue
Block a user