Files
glm-hub/SUPABASE-GLM-HUB-COMPLETO.sql
T

1069 lines
36 KiB
PL/PgSQL

-- ============================================================================
-- GLM HUB — ESQUEMA COMPLETO DE SUPABASE
-- ============================================================================
-- Incluye:
-- 1) Acceso automático por dominio, administradores y bloqueos excepcionales.
-- 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. ACCESO POR DOMINIO, ADMINISTRADORES Y BLOQUEOS
-- --------------------------------------------------------------------------
-- Regla vigente:
-- * Todo usuario autenticado con @gomezleemarketing.com entra como member.
-- * Un registro activo con role = 'admin' concede funciones administrativas.
-- * Un registro con is_active = false bloquea excepcionalmente ese correo.
-- * Los usuarios normales no necesitan una fila en esta tabla.
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'
)
)
);
comment on table public.glm_hub_authorized_users is
'Excepciones de acceso del GLM Hub: administradores activos y usuarios bloqueados. Los usuarios corporativos normales no requieren registro.';
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;
drop policy if exists "GLM Hub users can read their own control record"
on public.glm_hub_authorized_users;
-- Cada persona solo puede consultar su propia excepción. Esto permite que el
-- frontend detecte también un bloqueo (is_active = false) sin exponer a otros.
create policy "GLM Hub users can read their own control record"
on public.glm_hub_authorized_users
for select
to authenticated
using (
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();
-- Los registros member activos ya no son necesarios: la ausencia de fila
-- significa Usuario normal. Se conservan únicamente admins y bloqueos.
delete from public.glm_hub_authorized_users
where role = 'member'
and is_active = true;
create or replace function public.glm_hub_is_active_user()
returns boolean
language plpgsql
stable
security definer
set search_path = ''
as $$
declare
v_email text;
begin
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
return
auth.uid() is not null
and v_email ~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$'
and not exists (
select 1
from public.glm_hub_authorized_users as access
where access.email = v_email
and access.is_active = false
);
end;
$$;
create or replace function public.glm_hub_is_admin()
returns boolean
language plpgsql
stable
security definer
set search_path = ''
as $$
declare
v_email text;
begin
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
return
auth.uid() is not null
and exists (
select 1
from public.glm_hub_authorized_users as access
where access.email = v_email
and access.role = 'admin'
and access.is_active = true
);
end;
$$;
alter function public.glm_hub_is_active_user() owner to postgres;
alter function public.glm_hub_is_admin() owner to postgres;
-- n8n llama esta función usando el JWT recibido desde el frontend.
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 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_normalized_check
check (
approver_email = lower(btrim(approver_email))
and approver_email ~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-z0-9.-]+\.[a-z]{2,}$'
),
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_control_name text;
v_role text;
v_is_active boolean;
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;
if v_email !~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$' then
return jsonb_build_object('ok', false, 'error', 'Debes utilizar una cuenta corporativa de GomezLee Marketing.');
end if;
select access.full_name, access.role, access.is_active
into v_control_name, v_role, v_is_active
from public.glm_hub_authorized_users as access
where access.email = v_email
limit 1;
if found and v_is_active = false then
return jsonb_build_object('ok', false, 'error', 'Tu acceso al GLM Hub está deshabilitado. Contacta a IT Support.');
end if;
if coalesce(v_role, 'member') = 'admin' then
return jsonb_build_object('ok', false, 'error', 'Los administradores no necesitan solicitar acceso desde el Hub.');
end if;
v_name := coalesce(
nullif(btrim(v_control_name), ''),
nullif(btrim(auth.jwt() -> 'user_metadata' ->> 'full_name'), ''),
nullif(btrim(auth.jwt() -> 'user_metadata' ->> 'name'), ''),
split_part(v_email, '@', 1)
);
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 enlaces de decisión para los tres aprobadores base y,
-- cuando está disponible, para el jefe inmediato del solicitante. 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;
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;
with supervisor_candidate as (
select
coalesce(
nullif(btrim(employee.supervisor), ''),
nullif(btrim(supervisor_employee.name), ''),
split_part(lower(btrim(employee.supervisor_email)), '@', 1)
) as approver_name,
lower(btrim(employee.supervisor_email)) as approver_email,
40 as priority
from public.empleados_glm as employee
left join public.empleados_glm as supervisor_employee
on lower(btrim(supervisor_employee.work_email)) = lower(btrim(employee.supervisor_email))
where lower(btrim(employee.work_email)) = v_request.requester_email
and nullif(btrim(employee.supervisor_email), '') is not null
order by
case when employee.status::text = 'Active' then 0 else 1 end,
employee.id
limit 1
),
candidate_approvers as (
select 'Isaac Aracena'::text as approver_name,
'iaracena@gomezleemarketing.com'::text as approver_email,
10 as priority
union all
select 'José Leopoldo Gómez', 'jgomez@gomezleemarketing.com', 20
union all
select 'Máximo Gómez', 'mgomez@gomezleemarketing.com', 30
union all
select approver_name, approver_email, priority
from supervisor_candidate
),
deduplicated as (
select distinct on (approver_email)
approver_name,
approver_email,
priority
from candidate_approvers
where approver_email is not null
and approver_email <> ''
and approver_email <> v_request.requester_email
and approver_email ~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-z0-9.-]+\.[a-z]{2,}$'
order by approver_email, priority
),
tokenized as (
select
approver_name,
approver_email,
priority,
encode(extensions.gen_random_bytes(32), 'hex') as decision_token
from deduplicated
)
insert into public.glm_hub_access_request_approvers (
request_id,
approver_name,
approver_email,
decision_token,
decision_token_hash
)
select
p_request_id,
tokenized.approver_name,
tokenized.approver_email,
tokenized.decision_token,
encode(extensions.digest(tokenized.decision_token, 'sha256'), 'hex')
from tokenized
on conflict (request_id, approver_email) do nothing;
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 coalesce(
jsonb_agg(
jsonb_build_object(
'name', approver.approver_name,
'email', approver.approver_email,
'token', approver.decision_token
)
order by
case approver.approver_email
when 'iaracena@gomezleemarketing.com' then 1
when 'jgomez@gomezleemarketing.com' then 2
when 'mgomez@gomezleemarketing.com' then 3
else 4
end,
approver.approver_email
),
'[]'::jsonb
)
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. ESTADO DE ACCESO POR APLICACIÓN
-- --------------------------------------------------------------------------
create or replace function public.glm_hub_get_my_app_access()
returns table (
app_id uuid,
app_name text,
has_access boolean
)
language plpgsql
stable
security definer
set search_path = ''
as $$
declare
v_email text;
begin
v_email := lower(btrim(coalesce(auth.jwt() ->> 'email', '')));
if auth.uid() is null or v_email = '' then
return;
end if;
if v_email !~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$' then
return;
end if;
return query
select
app.id,
app.name,
case
-- Aplicaciones restringidas: consultar su fuente real de autorización.
when lower(btrim(app.name)) = 'cdc project management' then exists (
select 1
from public.tablero_cdc_allowed_users as allowed_user
where lower(btrim(allowed_user.email)) = v_email
and coalesce(allowed_user.is_active, false) = true
)
when lower(btrim(app.name)) = 'portal de verificación de nóminas' then exists (
select 1
from public.cruce_cuentas_usuarios_autorizados as allowed_user
where lower(btrim(allowed_user.email)) = v_email
)
when lower(btrim(app.name)) = 'seguimiento de impuestos glm' then exists (
select 1
from public.tax_calendar_access as allowed_user
where lower(btrim(allowed_user.email)) = v_email
and coalesce(allowed_user.active, false) = true
)
-- Aplicaciones disponibles para cualquier usuario GLM autenticado.
when lower(btrim(app.name)) in (
'bamboohr',
'cdc brief',
'cruce de seguridad social - honduras',
'cruce de seguridad social - guatemala',
'cruce de seguridad social - costa rica',
'cruce de seguridad social - república dominicana',
'cruce de seguridad social - el salvador',
'validación de ir - nicaragua',
'glm id card generator'
) then true
-- Seguridad por defecto: una aplicación nueva/no clasificada nunca se
-- muestra como autorizada hasta definir su fuente de permisos.
else false
end as has_access
from public.glm_hub_apps as app
where app.visibility = 'published'
order by app.name;
end;
$$;
alter function public.glm_hub_get_my_app_access() owner to postgres;
revoke all on function public.glm_hub_get_my_app_access() from public, anon;
grant execute on function public.glm_hub_get_my_app_access() to authenticated;
grant execute on function public.glm_hub_get_my_app_access() to service_role;
-- --------------------------------------------------------------------------
-- 8. 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)
-- ============================================================================
-- IMPORTANTE:
-- Todo correo @gomezleemarketing.com entra automáticamente como Usuario.
-- La tabla glm_hub_authorized_users se usa solo para administradores y bloqueos.
-- CONVERTIR UN USUARIO EN ADMINISTRADOR:
-- insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
-- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'admin', true)
-- on conflict (email) do update
-- set full_name = excluded.full_name, role = 'admin', is_active = true, updated_at = now();
-- DEVOLVER UN ADMINISTRADOR A USUARIO NORMAL:
-- delete from public.glm_hub_authorized_users
-- where email = 'usuario@gomezleemarketing.com';
-- BLOQUEAR EXCEPCIONALMENTE A UN USUARIO DEL DOMINIO:
-- insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
-- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'member', false)
-- on conflict (email) do update
-- set full_name = excluded.full_name, role = 'member', is_active = false, updated_at = now();
-- DESBLOQUEAR A UN USUARIO NORMAL:
-- delete from public.glm_hub_authorized_users
-- where email = 'usuario@gomezleemarketing.com'
-- and role = 'member';
-- VER ADMINISTRADORES Y BLOQUEOS:
-- select email, full_name, role, is_active, updated_at
-- from public.glm_hub_authorized_users
-- order by role, email;
-- 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;