build: actualizar dist con la versión de producción más reciente
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# GLM Hub · Accesos centralizados
|
||||
|
||||
La versión final utiliza `public.glm_hub_app_access` como **registro informativo único para el LED del Hub**. Esta tabla no reemplaza el login ni los permisos reales de las aplicaciones.
|
||||
|
||||
## Cómo se usa
|
||||
|
||||
- **Aplicación abierta a todo GLM:** una fila con el nombre de la aplicación y `email = *`.
|
||||
- **Aplicación restringida:** una fila por usuario con el nombre de la aplicación y su correo.
|
||||
- **Revocar acceso en el Hub:** eliminar la fila o cambiar `is_active` a `false`.
|
||||
- El `app_id` se completa automáticamente. Desde Supabase Table Editor solo necesitas completar `app_name` y `email`.
|
||||
|
||||
## Flujo operativo
|
||||
|
||||
Cuando IT otorgue acceso real a una aplicación restringida, debe agregar también al usuario en `glm_hub_app_access`. El Hub mostrará **Disponible** en verde. Si no existe un registro activo para ese usuario y tampoco existe una fila `*` para esa aplicación, mostrará **Sin acceso** en rojo.
|
||||
|
||||
## Aplicaciones actuales
|
||||
|
||||
El script final realiza una carga inicial automática: BambooHR, CDC Brief, cinco Cruces de Seguridad Social, Validación de IR - Nicaragua y GLM ID Card Generator quedan abiertos a todo GLM. CDC Project Management se copia inicialmente desde `tablero_cdc_allowed_users`; Portal de Verificación de Nóminas desde `cruce_cuentas_usuarios_autorizados`; y Seguimiento de Impuestos GLM desde `tax_calendar_access`.
|
||||
|
||||
Después de esa carga inicial, la fuente del LED es únicamente `glm_hub_app_access`. Los logins de las aplicaciones siguen funcionando como hoy.
|
||||
|
||||
## n8n
|
||||
|
||||
No se necesita ninguna modificación adicional en n8n para la tabla central. El workflow de solicitudes incluido en el ZIP conserva la mejora previa del jefe inmediato.
|
||||
@@ -0,0 +1,69 @@
|
||||
# GLM Hub — LED de acceso + jefe inmediato
|
||||
|
||||
## Qué cambia
|
||||
|
||||
Esta versión agrega dos mejoras sin cambiar el modelo actual de autenticación del Hub:
|
||||
|
||||
1. Cada aplicación publicada muestra el estado **Con acceso / Sin acceso** del usuario conectado.
|
||||
2. Las solicitudes de acceso se envían a **Isaac Aracena, José Leopoldo Gómez, Máximo Gómez y al jefe inmediato del solicitante**, cuando `empleados_glm.supervisor_email` está disponible.
|
||||
|
||||
La primera decisión registrada sigue siendo definitiva para todos los aprobadores.
|
||||
|
||||
## Fuentes de acceso utilizadas
|
||||
|
||||
El Hub no replica los permisos de las aplicaciones restringidas: consulta su fuente real en Supabase.
|
||||
|
||||
- **CDC Project Management** → `tablero_cdc_allowed_users` (`email`, `is_active`).
|
||||
- **Portal de Verificación de Nóminas** → `cruce_cuentas_usuarios_autorizados` (`email`).
|
||||
- **Seguimiento de Impuestos GLM** → `tax_calendar_access` (`email`, `active`).
|
||||
- **BambooHR, CDC Brief, los 5 Cruces de Seguridad Social, Validación de IR - Nicaragua y GLM ID Card Generator** → acceso general para usuarios autenticados `@gomezleemarketing.com`.
|
||||
- Cualquier aplicación futura que no esté clasificada se muestra por seguridad como **Sin acceso** hasta agregar su regla al RPC.
|
||||
|
||||
El estado se vuelve a comprobar al iniciar sesión, cada 60 segundos y cuando el usuario vuelve a enfocar la pestaña del Hub.
|
||||
|
||||
## Orden de implementación
|
||||
|
||||
### 1. Supabase
|
||||
|
||||
Ejecutar completo:
|
||||
|
||||
`SUPABASE-GLM-HUB-LED-ACCESO-JEFE-INMEDIATO.sql`
|
||||
|
||||
Este script:
|
||||
|
||||
- elimina la restricción que limitaba los aprobadores a tres correos exactos;
|
||||
- conserva a los tres aprobadores base;
|
||||
- obtiene el jefe desde `empleados_glm.work_email → supervisor_email`;
|
||||
- evita duplicados;
|
||||
- no rompe la solicitud si el jefe no está disponible;
|
||||
- crea el RPC `glm_hub_get_my_app_access()` para el LED.
|
||||
|
||||
### 2. n8n
|
||||
|
||||
Actualizar el workflow **GLM Hub - Solicitudes de acceso y aprobación** con:
|
||||
|
||||
`n8n/GLM-Hub-Solicitudes-Acceso-Aprobacion.json`
|
||||
|
||||
No se modificó el workflow de Gemini. Tampoco se cambió la forma actual en que el workflow usa las credenciales/keys de Supabase, por solicitud expresa.
|
||||
|
||||
### 3. Frontend
|
||||
|
||||
Publicar el contenido nuevo de `dist/` en el mismo subdominio del GLM Hub.
|
||||
|
||||
## Prueba recomendada
|
||||
|
||||
1. Iniciar sesión con un usuario GLM que no sea administrador.
|
||||
2. Confirmar que cada app muestre **Con acceso** o **Sin acceso**.
|
||||
3. Verificar un usuario presente y otro ausente en cada una de las tres tablas restringidas.
|
||||
4. Enviar una solicitud de acceso.
|
||||
5. En la ejecución de n8n, revisar `Emitir enlaces de aprobación`: debe devolver 3 aprobadores base y, cuando exista, un cuarto aprobador correspondiente al `supervisor_email`.
|
||||
6. Confirmar que todos reciban su correo.
|
||||
7. Aprobar con uno de ellos y comprobar que un segundo intento muestre que la solicitud ya fue atendida.
|
||||
8. Agregar/eliminar al usuario en la tabla real de una app restringida y volver al Hub; al recuperar el foco (o como máximo en 60 segundos) el LED debe actualizarse.
|
||||
|
||||
## Comportamiento de seguridad
|
||||
|
||||
- Un usuario sin registro en `glm_hub_authorized_users` sigue entrando al Hub si pertenece a `@gomezleemarketing.com`.
|
||||
- `glm_hub_authorized_users` continúa usándose para administradores y bloqueos excepcionales.
|
||||
- El LED es informativo: no sustituye la protección interna de cada aplicación.
|
||||
- Las aplicaciones no clasificadas no se marcan automáticamente como autorizadas.
|
||||
+159
-45
@@ -546,13 +546,10 @@ create table if not exists public.glm_hub_access_request_approvers (
|
||||
clicked_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
|
||||
constraint glm_hub_access_request_approvers_email_check
|
||||
constraint glm_hub_access_request_approvers_email_normalized_check
|
||||
check (
|
||||
approver_email in (
|
||||
'iaracena@gomezleemarketing.com',
|
||||
'jgomez@gomezleemarketing.com',
|
||||
'mgomez@gomezleemarketing.com'
|
||||
)
|
||||
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)
|
||||
@@ -696,8 +693,9 @@ revoke all on function public.glm_hub_prepare_access_request(uuid) from public,
|
||||
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.
|
||||
-- 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
|
||||
@@ -707,9 +705,6 @@ 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
|
||||
@@ -722,15 +717,57 @@ begin
|
||||
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');
|
||||
|
||||
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,
|
||||
@@ -738,29 +775,14 @@ begin
|
||||
decision_token,
|
||||
decision_token_hash
|
||||
)
|
||||
values
|
||||
(
|
||||
select
|
||||
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;
|
||||
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,
|
||||
@@ -770,13 +792,23 @@ begin
|
||||
'appName', v_request.app_name,
|
||||
'appCategory', v_request.app_category,
|
||||
'approvers', (
|
||||
select jsonb_agg(
|
||||
select coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', approver.approver_name,
|
||||
'email', approver.approver_email,
|
||||
'token', approver.decision_token
|
||||
)
|
||||
order by approver.approver_email
|
||||
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
|
||||
@@ -886,7 +918,89 @@ grant execute on function public.glm_hub_decide_access_request(text, text) to au
|
||||
grant execute on function public.glm_hub_decide_access_request(text, text) to service_role;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 7. REALTIME DEL CATÁLOGO
|
||||
-- 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
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
-- ============================================================================
|
||||
-- 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';
|
||||
@@ -0,0 +1,487 @@
|
||||
-- ============================================================================
|
||||
-- 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';
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+179
File diff suppressed because one or more lines are too long
Vendored
-179
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -9,8 +9,8 @@
|
||||
<link rel="shortcut icon" type="image/png" href="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png?v=glm-hub-20260722-v3" />
|
||||
<link rel="apple-touch-icon" href="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png?v=glm-hub-20260722-v3" />
|
||||
<title>GLM Hub — GomezLee Marketing</title>
|
||||
<script type="module" crossorigin src="/assets/index-Kd6NFVcJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-9ByAKvcq.css">
|
||||
<script type="module" crossorigin src="/assets/index-CnXPZvfE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CIGqNuJ5.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ const root = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
const srcDir = path.join(root, 'src');
|
||||
const distDir = path.join(root, 'dist');
|
||||
const assetsDir = path.join(distDir, 'assets');
|
||||
const BUILD_VERSION = '20260814-single-access-status';
|
||||
|
||||
const env = {
|
||||
VITE_SUPABASE_URL: 'https://dbit.digitalcompass.agency',
|
||||
@@ -29,7 +30,7 @@ function fixLocalImports(code) {
|
||||
.replace(/^import\s+["']\.\/styles\.css["'];?\s*$/gm, '')
|
||||
.replace(/(from\s+["'])(\.\.?\/[^"']+)(["'])/g, (_m, start, spec, end) => {
|
||||
if (/\.(?:js|mjs|cjs|json|css)$/.test(spec)) return `${start}${spec}${end}`;
|
||||
return `${start}${spec}.js${end}`;
|
||||
return `${start}${spec}.js?v=${BUILD_VERSION}${end}`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ const indexHtml = `<!doctype html>
|
||||
<link rel="shortcut icon" type="image/png" href="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png?v=glm-hub-20260728-access" />
|
||||
<link rel="apple-touch-icon" href="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png?v=glm-hub-20260728-access" />
|
||||
<title>GLM Hub — GomezLee Marketing</title>
|
||||
<link rel="stylesheet" href="/assets/styles.css" />
|
||||
<link rel="stylesheet" href="/assets/styles.css?v=${BUILD_VERSION}" />
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
@@ -91,7 +92,7 @@ const indexHtml = `<!doctype html>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.110.8"></script>
|
||||
<script type="module" src="/assets/main.js"></script>
|
||||
<script type="module" src="/assets/main.js?v=${BUILD_VERSION}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
fs.writeFileSync(path.join(distDir, 'index.html'), indexHtml);
|
||||
|
||||
+74
-7
@@ -35,6 +35,7 @@ import {
|
||||
deleteHubApp,
|
||||
fetchHubApps,
|
||||
fetchHubData,
|
||||
fetchMyAppAccess,
|
||||
migrateLocalAppsIfNeeded,
|
||||
recordRecent,
|
||||
saveHubApp,
|
||||
@@ -536,14 +537,29 @@ function MobileNav({ user, view, onNavigate, onLogout }: MobileNavProps) {
|
||||
);
|
||||
}
|
||||
|
||||
type AccessState = boolean | null;
|
||||
|
||||
function AccessIndicator({ hasAccess }: { hasAccess: AccessState }) {
|
||||
const label = hasAccess === null ? "Verificando acceso" : hasAccess ? "Disponible" : "Sin acceso";
|
||||
const stateClass = hasAccess === null ? "access-indicator--checking" : hasAccess ? "access-indicator--granted" : "access-indicator--denied";
|
||||
|
||||
return (
|
||||
<span className={`access-indicator ${stateClass}`} aria-label={label}>
|
||||
<span aria-hidden="true" />
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuickLaunchProps {
|
||||
app: HubApp;
|
||||
isFavorite: boolean;
|
||||
hasAccess: AccessState;
|
||||
onActivate: (app: HubApp) => void;
|
||||
onFavorite: (appId: string) => void;
|
||||
}
|
||||
|
||||
function QuickLaunch({ app, isFavorite, onActivate, onFavorite }: QuickLaunchProps) {
|
||||
function QuickLaunch({ app, isFavorite, hasAccess, onActivate, onFavorite }: QuickLaunchProps) {
|
||||
const available = Boolean(getSafeUrl(app.url));
|
||||
return (
|
||||
<article className="quick-launch">
|
||||
@@ -560,7 +576,10 @@ function QuickLaunch({ app, isFavorite, onActivate, onFavorite }: QuickLaunchPro
|
||||
</button>
|
||||
</div>
|
||||
<div className="quick-launch__copy">
|
||||
<div className="quick-launch__meta">
|
||||
<span className="quick-launch__category">{app.category}</span>
|
||||
<AccessIndicator hasAccess={hasAccess} />
|
||||
</div>
|
||||
<h3>{app.name}</h3>
|
||||
<p>{app.description}</p>
|
||||
</div>
|
||||
@@ -576,11 +595,12 @@ interface DirectoryRowProps {
|
||||
app: HubApp;
|
||||
index: number;
|
||||
isFavorite: boolean;
|
||||
hasAccess: AccessState;
|
||||
onActivate: (app: HubApp) => void;
|
||||
onFavorite: (appId: string) => void;
|
||||
}
|
||||
|
||||
function DirectoryRow({ app, index, isFavorite, onActivate, onFavorite }: DirectoryRowProps) {
|
||||
function DirectoryRow({ app, index, isFavorite, hasAccess, onActivate, onFavorite }: DirectoryRowProps) {
|
||||
const available = Boolean(getSafeUrl(app.url));
|
||||
return (
|
||||
<article className="directory-row">
|
||||
@@ -591,9 +611,8 @@ function DirectoryRow({ app, index, isFavorite, onActivate, onFavorite }: Direct
|
||||
<p>{app.description}</p>
|
||||
</div>
|
||||
<span className="directory-row__category">{app.category}</span>
|
||||
<span className={`availability ${available ? "availability--ready" : "availability--pending"}`}>
|
||||
<span aria-hidden="true" />
|
||||
{available ? "Disponible" : "Enlace pendiente"}
|
||||
<span className="directory-row__status">
|
||||
<AccessIndicator hasAccess={hasAccess} />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -880,11 +899,13 @@ function Pagination({ currentPage, totalPages, onPageChange, compact = false, la
|
||||
|
||||
const HUB_ITEMS_PER_PAGE = 10;
|
||||
const QUICK_ITEMS_PER_PAGE = 6;
|
||||
const ADMIN_ITEMS_PER_PAGE = 10;
|
||||
const ADMIN_ITEMS_PER_PAGE = 12;
|
||||
|
||||
interface HubViewProps {
|
||||
user: HubUser;
|
||||
data: HubData;
|
||||
accessByApp: Record<string, boolean>;
|
||||
accessReady: boolean;
|
||||
storageWarning: string;
|
||||
onActivate: (app: HubApp) => void;
|
||||
onFavorite: (appId: string) => void;
|
||||
@@ -892,7 +913,7 @@ interface HubViewProps {
|
||||
onRequestAccess: (app: HubApp) => Promise<boolean>;
|
||||
}
|
||||
|
||||
function HubView({ user, data, storageWarning, onActivate, onFavorite, onAdmin, onRequestAccess }: HubViewProps) {
|
||||
function HubView({ user, data, accessByApp, accessReady, storageWarning, onActivate, onFavorite, onAdmin, onRequestAccess }: HubViewProps) {
|
||||
const [categorySearches, setCategorySearches] = useState<Record<string, string>>({
|
||||
Todas: "",
|
||||
Administración: "",
|
||||
@@ -1068,6 +1089,7 @@ function HubView({ user, data, storageWarning, onActivate, onFavorite, onAdmin,
|
||||
key={app.id}
|
||||
app={app}
|
||||
isFavorite={favorites.has(app.id)}
|
||||
hasAccess={accessReady ? accessByApp[app.id] === true : null}
|
||||
onActivate={onActivate}
|
||||
onFavorite={onFavorite}
|
||||
/>
|
||||
@@ -1152,6 +1174,7 @@ function HubView({ user, data, storageWarning, onActivate, onFavorite, onAdmin,
|
||||
app={app}
|
||||
index={(currentPage - 1) * HUB_ITEMS_PER_PAGE + index}
|
||||
isFavorite={favorites.has(app.id)}
|
||||
hasAccess={accessReady ? accessByApp[app.id] === true : null}
|
||||
onActivate={onActivate}
|
||||
onFavorite={onFavorite}
|
||||
/>
|
||||
@@ -1861,6 +1884,8 @@ function App() {
|
||||
const [authReady, setAuthReady] = useState(false);
|
||||
const [catalogReady, setCatalogReady] = useState(false);
|
||||
const [catalogWarning, setCatalogWarning] = useState("");
|
||||
const [accessByApp, setAccessByApp] = useState<Record<string, boolean>>({});
|
||||
const [accessReady, setAccessReady] = useState(false);
|
||||
const [loginError, setLoginError] = useState("");
|
||||
const authorizationSequence = useRef(0);
|
||||
const authorizedUserIdRef = useRef<string | null>(null);
|
||||
@@ -2011,6 +2036,44 @@ function App() {
|
||||
};
|
||||
}, [activeUser?.id, activeUser?.role, initialLocalData.data.apps, notify]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeUser) {
|
||||
setAccessByApp({});
|
||||
setAccessReady(false);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
let loading = false;
|
||||
|
||||
const refreshAccess = async () => {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
const accessMap = await fetchMyAppAccess();
|
||||
if (!active) return;
|
||||
setAccessByApp(accessMap);
|
||||
setAccessReady(true);
|
||||
} catch (error) {
|
||||
console.warn("No se pudo verificar el acceso a las aplicaciones:", error);
|
||||
if (active) setAccessReady(false);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
void refreshAccess();
|
||||
const intervalId = window.setInterval(() => void refreshAccess(), 60_000);
|
||||
const handleFocus = () => void refreshAccess();
|
||||
window.addEventListener("focus", handleFocus);
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
window.clearInterval(intervalId);
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
};
|
||||
}, [activeUser?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const syncViewWithHash = () => {
|
||||
const requestedView: AppView = window.location.hash === "#admin" ? "admin" : "hub";
|
||||
@@ -2240,6 +2303,8 @@ function App() {
|
||||
}
|
||||
setSession(null);
|
||||
setCatalogReady(false);
|
||||
setAccessByApp({});
|
||||
setAccessReady(false);
|
||||
setView("hub");
|
||||
window.history.replaceState(null, "", getAppUrl());
|
||||
};
|
||||
@@ -2281,6 +2346,8 @@ function App() {
|
||||
<HubView
|
||||
user={session.user}
|
||||
data={data}
|
||||
accessByApp={accessByApp}
|
||||
accessReady={accessReady}
|
||||
storageWarning={catalogWarning}
|
||||
onActivate={handleActivate}
|
||||
onFavorite={(appId) => void handleFavorite(appId)}
|
||||
|
||||
@@ -97,6 +97,18 @@ async function removeStoredIcon(url: string): Promise<void> {
|
||||
if (error) console.warn("No se pudo retirar el ícono anterior de Storage:", error);
|
||||
}
|
||||
|
||||
|
||||
export type HubAccessMap = Record<string, boolean>;
|
||||
|
||||
export async function fetchMyAppAccess(): Promise<HubAccessMap> {
|
||||
const { data, error } = await supabase.rpc("glm_hub_get_my_app_access");
|
||||
|
||||
if (error) throw new Error(`No se pudo comprobar el acceso a las aplicaciones: ${error.message}`);
|
||||
|
||||
const rows = (data ?? []) as Array<{ app_id: string; has_access: boolean }>;
|
||||
return Object.fromEntries(rows.map((row) => [row.app_id, row.has_access === true]));
|
||||
}
|
||||
|
||||
export async function fetchHubApps(): Promise<HubApp[]> {
|
||||
const { data, error } = await supabase
|
||||
.from(APPS_TABLE)
|
||||
|
||||
+70
-2
@@ -1069,6 +1069,60 @@ img {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
|
||||
.quick-launch__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.access-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--muted);
|
||||
font-size: 0.67rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.access-indicator > span {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.access-indicator--granted {
|
||||
color: #4f758b;
|
||||
}
|
||||
|
||||
.access-indicator--granted > span {
|
||||
background: var(--glm-green);
|
||||
box-shadow: 0 0 0 3px var(--green-soft);
|
||||
}
|
||||
|
||||
.access-indicator--denied {
|
||||
color: #b3261e;
|
||||
}
|
||||
|
||||
.access-indicator--denied > span {
|
||||
background: #d32f2f;
|
||||
box-shadow: 0 0 0 3px #fde8e7;
|
||||
}
|
||||
|
||||
.access-indicator--checking {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
.access-indicator--checking > span {
|
||||
background: #a8a8a8;
|
||||
box-shadow: 0 0 0 3px #f0f0f0;
|
||||
}
|
||||
|
||||
.quick-launch__category {
|
||||
color: var(--glm-blue);
|
||||
font-size: 0.64rem;
|
||||
@@ -1366,6 +1420,13 @@ img {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.directory-row__status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.directory-row__category {
|
||||
color: var(--glm-blue);
|
||||
font-size: 0.65rem;
|
||||
@@ -1921,6 +1982,10 @@ img {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.ai-icon-button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ai-icon-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2274,7 +2339,9 @@ img {
|
||||
grid-template-columns: 34px 44px minmax(190px, 1fr) 112px 44px 80px;
|
||||
}
|
||||
|
||||
.directory-row__category {
|
||||
|
||||
|
||||
.directory-row__category {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -2624,7 +2691,8 @@ img {
|
||||
}
|
||||
|
||||
.directory-row__category,
|
||||
.directory-row .availability {
|
||||
.directory-row .availability,
|
||||
.directory-row__status {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user