Files
glm-hub/SUPABASE-GLM-HUB-FINAL-LED-JEFE-ACCESOS-CENTRALIZADOS.sql
T

488 lines
16 KiB
PL/PgSQL

-- ============================================================================
-- GLM HUB · VERSIÓN FINAL
-- LED DE DISPONIBILIDAD + REGISTRO CENTRAL DE ACCESOS + JEFE INMEDIATO
-- GomezLee Marketing
-- Fecha: 2026-08-14
--
-- OBJETIVO
-- 1) Mantener a Isaac + José Leopoldo + Máximo como aprobadores base.
-- 2) Agregar al jefe inmediato desde empleados_glm.supervisor_email.
-- 3) Crear UNA tabla central que el Hub usa únicamente para saber si debe mostrar
-- "Disponible" o "Sin acceso".
-- 4) NO modifica el login ni los permisos reales de ninguna aplicación.
-- 5) NO modifica claves, credenciales ni configuración de EasyPanel/n8n.
--
-- TABLA CENTRAL
-- public.glm_hub_app_access
-- app_name = nombre visible de la aplicación en el Hub
-- email = correo autorizado
-- email='*' significa que TODOS los usuarios @gomezleemarketing.com tienen acceso
-- is_active=false permite desactivar el registro sin borrarlo
--
-- OPERACIÓN FUTURA
-- - App abierta a todo GLM: agregar una fila con app_name + email='*'.
-- - App restringida: agregar una fila por usuario con app_name + su correo.
-- - Revocar: borrar la fila o cambiar is_active a false.
-- ============================================================================
begin;
-- --------------------------------------------------------------------------
-- 1. APROBADORES DINÁMICOS: 3 BASE + JEFE INMEDIATO
-- --------------------------------------------------------------------------
alter table public.glm_hub_access_request_approvers
drop constraint if exists glm_hub_access_request_approvers_email_check;
alter table public.glm_hub_access_request_approvers
drop constraint if exists glm_hub_access_request_approvers_email_normalized_check;
alter table public.glm_hub_access_request_approvers
add 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,}$'
);
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;
-- --------------------------------------------------------------------------
-- 2. TABLA CENTRAL DE ACCESOS DEL HUB
-- --------------------------------------------------------------------------
-- Esta tabla es INFORMATIVA para el Hub. No sustituye el login/seguridad real
-- de cada aplicación.
--
-- app_id es rellenado automáticamente a partir de app_name, de forma que desde
-- el Table Editor basta con escribir el nombre de la app y el correo.
-- --------------------------------------------------------------------------
create table if not exists public.glm_hub_app_access (
id uuid primary key default extensions.gen_random_uuid(),
app_name text not null,
email text not null,
is_active boolean not null default true,
app_id uuid references public.glm_hub_apps(id) on delete cascade,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint glm_hub_app_access_email_check
check (
email = '*'
or email ~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-z0-9.-]+\.[a-z]{2,}$'
)
);
comment on table public.glm_hub_app_access is
'Registro central informativo de accesos mostrado por GLM Hub. No reemplaza los permisos reales de cada aplicación.';
comment on column public.glm_hub_app_access.app_name is
'Nombre de la aplicación tal como aparece en glm_hub_apps.';
comment on column public.glm_hub_app_access.email is
'Correo autorizado. El valor * significa todos los usuarios GLM autenticados.';
comment on column public.glm_hub_app_access.is_active is
'TRUE = acceso mostrado como Disponible; FALSE = registro desactivado.';
comment on column public.glm_hub_app_access.app_id is
'Se completa automáticamente usando app_name.';
create unique index if not exists glm_hub_app_access_app_email_uidx
on public.glm_hub_app_access (app_id, email);
create index if not exists glm_hub_app_access_email_idx
on public.glm_hub_app_access (email)
where is_active = true;
-- Normaliza correos y permite trabajar por nombre de aplicación desde Table Editor.
create or replace function public.glm_hub_app_access_prepare_row()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
v_app_id uuid;
v_app_name text;
begin
new.app_name := btrim(coalesce(new.app_name, ''));
new.email := lower(btrim(coalesce(new.email, '')));
if new.app_name = '' then
raise exception 'Debes indicar app_name.';
end if;
if new.email = '' then
raise exception 'Debes indicar email. Usa * si la aplicación está disponible para todo GLM.';
end if;
-- Si el usuario cambió app_name manualmente, ese nombre manda y se resuelve
-- nuevamente el UUID. En INSERT también permite dejar app_id vacío.
if tg_op = 'INSERT' then
select app.id, app.name
into v_app_id, v_app_name
from public.glm_hub_apps as app
where lower(btrim(app.name)) = lower(new.app_name)
order by app.created_at
limit 1;
if v_app_id is null then
raise exception 'No existe una aplicación en GLM Hub llamada "%".', new.app_name;
end if;
new.app_id := v_app_id;
new.app_name := v_app_name;
elsif new.app_id is null or new.app_name is distinct from old.app_name then
select app.id, app.name
into v_app_id, v_app_name
from public.glm_hub_apps as app
where lower(btrim(app.name)) = lower(new.app_name)
order by app.created_at
limit 1;
if v_app_id is null then
raise exception 'No existe una aplicación en GLM Hub llamada "%".', new.app_name;
end if;
new.app_id := v_app_id;
new.app_name := v_app_name;
else
select app.name
into v_app_name
from public.glm_hub_apps as app
where app.id = new.app_id;
if v_app_name is null then
raise exception 'El app_id indicado no existe en glm_hub_apps.';
end if;
new.app_name := v_app_name;
end if;
new.updated_at := now();
return new;
end;
$$;
drop trigger if exists glm_hub_app_access_prepare_row_trigger
on public.glm_hub_app_access;
create trigger glm_hub_app_access_prepare_row_trigger
before insert or update on public.glm_hub_app_access
for each row
execute function public.glm_hub_app_access_prepare_row();
-- Si un administrador cambia el nombre de una app en el Hub, el nombre visible
-- de los registros centrales se mantiene sincronizado por app_id.
create or replace function public.glm_hub_sync_access_app_name()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
if new.name is distinct from old.name then
update public.glm_hub_app_access
set app_name = new.name,
updated_at = now()
where app_id = new.id;
end if;
return new;
end;
$$;
drop trigger if exists glm_hub_sync_access_app_name_trigger
on public.glm_hub_apps;
create trigger glm_hub_sync_access_app_name_trigger
after update of name on public.glm_hub_apps
for each row
execute function public.glm_hub_sync_access_app_name();
-- Nadie necesita leer esta tabla directamente desde el navegador. El frontend
-- consulta únicamente el RPC seguro glm_hub_get_my_app_access().
alter table public.glm_hub_app_access enable row level security;
revoke all on table public.glm_hub_app_access from anon, authenticated;
grant all on table public.glm_hub_app_access to service_role;
-- --------------------------------------------------------------------------
-- 3. CARGA INICIAL DE LOS ACCESOS ACTUALES
-- --------------------------------------------------------------------------
-- A) Apps abiertas para cualquier usuario GLM -> una sola fila con email='*'.
-- --------------------------------------------------------------------------
insert into public.glm_hub_app_access (app_id, app_name, email, is_active)
select app.id, app.name, '*', true
from public.glm_hub_apps as app
where 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'
)
on conflict (app_id, email)
do update set
app_name = excluded.app_name,
is_active = true,
updated_at = now();
-- B) CDC Project Management -> snapshot inicial desde tablero_cdc_allowed_users.
insert into public.glm_hub_app_access (app_id, app_name, email, is_active)
select
app.id,
app.name,
lower(btrim(allowed_user.email)),
true
from public.glm_hub_apps as app
cross join public.tablero_cdc_allowed_users as allowed_user
where lower(btrim(app.name)) = 'cdc project management'
and nullif(btrim(allowed_user.email), '') is not null
and coalesce(allowed_user.is_active, false) = true
on conflict (app_id, email)
do update set
app_name = excluded.app_name,
is_active = true,
updated_at = now();
-- C) Portal de Verificación de Nóminas -> snapshot inicial desde la tabla actual.
insert into public.glm_hub_app_access (app_id, app_name, email, is_active)
select
app.id,
app.name,
lower(btrim(allowed_user.email)),
true
from public.glm_hub_apps as app
cross join public.cruce_cuentas_usuarios_autorizados as allowed_user
where lower(btrim(app.name)) = 'portal de verificación de nóminas'
and nullif(btrim(allowed_user.email), '') is not null
on conflict (app_id, email)
do update set
app_name = excluded.app_name,
is_active = true,
updated_at = now();
-- D) Seguimiento de Impuestos GLM -> snapshot inicial desde tax_calendar_access.
insert into public.glm_hub_app_access (app_id, app_name, email, is_active)
select
app.id,
app.name,
lower(btrim(allowed_user.email)),
true
from public.glm_hub_apps as app
cross join public.tax_calendar_access as allowed_user
where lower(btrim(app.name)) = 'seguimiento de impuestos glm'
and nullif(btrim(allowed_user.email), '') is not null
and coalesce(allowed_user.active, false) = true
on conflict (app_id, email)
do update set
app_name = excluded.app_name,
is_active = true,
updated_at = now();
-- --------------------------------------------------------------------------
-- 4. RPC FINAL PARA EL LED DEL HUB
-- --------------------------------------------------------------------------
-- El Hub ya NO consulta las tablas particulares de cada aplicación.
-- Solo lee este registro central.
-- --------------------------------------------------------------------------
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;
-- El GLM Hub continúa reservado al dominio corporativo principal.
if v_email !~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$' then
return;
end if;
return query
select
app.id,
app.name,
exists (
select 1
from public.glm_hub_app_access as access_row
where access_row.app_id = app.id
and access_row.is_active = true
and (
access_row.email = '*'
or access_row.email = v_email
)
) 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;
commit;
-- ============================================================================
-- VERIFICACIONES OPCIONALES DESPUÉS DE EJECUTAR
-- ============================================================================
-- Ver toda la matriz central:
-- select app_name, email, is_active
-- from public.glm_hub_app_access
-- order by app_name, email;
-- Agregar acceso individual a una futura app restringida DESDE SQL:
-- insert into public.glm_hub_app_access (app_name, email)
-- values ('Nombre exacto de la aplicación', 'usuario@gomezleemarketing.com');
--
-- En el Table Editor puedes hacer exactamente lo mismo dejando app_id vacío.
-- Marcar una futura app como abierta para todo GLM:
-- insert into public.glm_hub_app_access (app_name, email)
-- values ('Nombre exacto de la aplicación', '*');
-- Revocar sin borrar historial:
-- update public.glm_hub_app_access
-- set is_active = false
-- where lower(app_name) = lower('Nombre exacto de la aplicación')
-- and email = 'usuario@gomezleemarketing.com';