488 lines
16 KiB
PL/PgSQL
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';
|