feat: habilitar acceso automático al Hub por dominio
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
# GLM Hub — Acceso automático por dominio
|
||||||
|
|
||||||
|
## Regla final
|
||||||
|
|
||||||
|
- Cualquier cuenta autenticada con `@gomezleemarketing.com` entra al GLM Hub como **Usuario**.
|
||||||
|
- Los permisos para abrir cada aplicación se controlan dentro del login de esa aplicación.
|
||||||
|
- `glm_hub_authorized_users` se utiliza únicamente para:
|
||||||
|
- los cinco administradores del Hub;
|
||||||
|
- bloquear excepcionalmente una cuenta corporativa.
|
||||||
|
|
||||||
|
## Actualización de una instalación existente
|
||||||
|
|
||||||
|
1. Ejecutar en Supabase SQL Editor: `SUPABASE-GLM-HUB-ACCESO-POR-DOMINIO.sql`.
|
||||||
|
2. Publicar el `dist` incluido en este proyecto.
|
||||||
|
3. Cerrar sesión y volver a entrar para comprobar el rol.
|
||||||
|
|
||||||
|
No es necesario modificar los workflows de n8n.
|
||||||
|
|
||||||
|
## Semántica de la tabla
|
||||||
|
|
||||||
|
| Registro | Resultado |
|
||||||
|
|---|---|
|
||||||
|
| No existe una fila | Usuario normal con acceso al Hub |
|
||||||
|
| `role = admin`, `is_active = true` | Administrador |
|
||||||
|
| `role = member`, `is_active = false` | Usuario bloqueado |
|
||||||
|
|
||||||
|
Los registros `member` activos se eliminan durante la migración porque ya no son necesarios.
|
||||||
@@ -20,7 +20,8 @@ Luego ejecútalo una sola vez. El script es reutilizable: usa `if not exists`, r
|
|||||||
|
|
||||||
El script configura:
|
El script configura:
|
||||||
|
|
||||||
- Lista de usuarios autorizados y roles.
|
- Acceso automático para el dominio `@gomezleemarketing.com`.
|
||||||
|
- Tabla de administradores y bloqueos excepcionales.
|
||||||
- Los cinco administradores definidos para GLM Hub.
|
- Los cinco administradores definidos para GLM Hub.
|
||||||
- Catálogo compartido `glm_hub_apps`.
|
- Catálogo compartido `glm_hub_apps`.
|
||||||
- Favoritos personales `glm_hub_favorites`.
|
- Favoritos personales `glm_hub_favorites`.
|
||||||
@@ -118,24 +119,30 @@ Mientras Gemini está trabajando:
|
|||||||
- No se puede publicar, cancelar, editar ni salir accidentalmente de la página.
|
- No se puede publicar, cancelar, editar ni salir accidentalmente de la página.
|
||||||
- Al finalizar, el ícono aparece en la vista previa y puede publicarse junto con la aplicación.
|
- Al finalizar, el ícono aparece en la vista previa y puede publicarse junto con la aplicación.
|
||||||
|
|
||||||
## Dar o quitar acceso
|
## Administradores y bloqueos
|
||||||
|
|
||||||
Desde `public.glm_hub_authorized_users`:
|
Todo correo `@gomezleemarketing.com` entra como Usuario sin requerir una fila. La tabla `public.glm_hub_authorized_users` se utiliza únicamente así:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Dar acceso normal
|
-- Convertir en administrador
|
||||||
insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||||
values ('persona@gomezleemarketing.com', 'Nombre de la persona', 'member', true);
|
values ('persona@gomezleemarketing.com', 'Nombre de la persona', 'admin', true)
|
||||||
|
on conflict (email) do update
|
||||||
|
set full_name = excluded.full_name, role = 'admin', is_active = true, updated_at = now();
|
||||||
|
|
||||||
-- Quitar acceso sin borrar
|
-- Volver a usuario normal
|
||||||
update public.glm_hub_authorized_users
|
delete from public.glm_hub_authorized_users
|
||||||
set is_active = false
|
|
||||||
where email = 'persona@gomezleemarketing.com';
|
where email = 'persona@gomezleemarketing.com';
|
||||||
|
|
||||||
-- Reactivar
|
-- Bloquear excepcionalmente
|
||||||
update public.glm_hub_authorized_users
|
insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||||
set is_active = true
|
values ('persona@gomezleemarketing.com', 'Nombre de la persona', 'member', false)
|
||||||
where email = 'persona@gomezleemarketing.com';
|
on conflict (email) do update
|
||||||
|
set full_name = excluded.full_name, role = 'member', is_active = false, updated_at = now();
|
||||||
```
|
```
|
||||||
|
|
||||||
Los administradores están restringidos por una validación de base de datos a los cinco correos definidos en el script.
|
Los administradores están restringidos por una validación de base de datos a los cinco correos definidos en el script.
|
||||||
|
|
||||||
|
## Regla de acceso al Hub
|
||||||
|
|
||||||
|
Cualquier cuenta `@gomezleemarketing.com` puede iniciar sesión como Usuario. La tabla `glm_hub_authorized_users` se reserva para administradores y bloqueos excepcionales; el acceso específico a cada portal se administra dentro de esa aplicación.
|
||||||
|
|||||||
@@ -9,3 +9,9 @@ Ejecuta únicamente el script completo:
|
|||||||
`SUPABASE-GLM-HUB-COMPLETO.sql`
|
`SUPABASE-GLM-HUB-COMPLETO.sql`
|
||||||
|
|
||||||
El catálogo, los íconos, favoritos, recientes, RLS y Realtime están incluidos en ese único archivo.
|
El catálogo, los íconos, favoritos, recientes, RLS y Realtime están incluidos en ese único archivo.
|
||||||
|
|
||||||
|
## Acceso por dominio
|
||||||
|
|
||||||
|
Todo correo `@gomezleemarketing.com` entra automáticamente como **Usuario**. La tabla `glm_hub_authorized_users` no funciona como lista blanca general: se usa únicamente para los cinco administradores autorizados y para bloqueos excepcionales (`role = member`, `is_active = false`).
|
||||||
|
|
||||||
|
Para actualizar una instalación existente, ejecuta `SUPABASE-GLM-HUB-ACCESO-POR-DOMINIO.sql`.
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ Usuario
|
|||||||
| Build | Vite 6 | Desarrollo, validación y compilación |
|
| Build | Vite 6 | Desarrollo, validación y compilación |
|
||||||
| UI | CSS + Lucide React | Estilos e iconografía |
|
| UI | CSS + Lucide React | Estilos e iconografía |
|
||||||
| Autenticación | Supabase Auth + Google OAuth | Inicio de sesión corporativo |
|
| Autenticación | Supabase Auth + Google OAuth | Inicio de sesión corporativo |
|
||||||
| Base de datos | Supabase / PostgreSQL | Catálogo, usuarios, favoritos, recientes y solicitudes |
|
| Base de datos | Supabase / PostgreSQL | Catálogo, administradores/bloqueos, favoritos, recientes y solicitudes |
|
||||||
| Seguridad | Row Level Security | Control de lectura y escritura según usuario y rol |
|
| Seguridad | Row Level Security | Control de lectura y escritura según usuario y rol |
|
||||||
| Archivos | Supabase Storage | Almacenamiento de íconos |
|
| Archivos | Supabase Storage | Almacenamiento de íconos |
|
||||||
| Sincronización | Supabase Realtime | Actualización del catálogo en sesiones abiertas |
|
| Sincronización | Supabase Realtime | Actualización del catálogo en sesiones abiertas |
|
||||||
@@ -153,7 +153,7 @@ Usuario
|
|||||||
|
|
||||||
## 📐 Reglas de negocio
|
## 📐 Reglas de negocio
|
||||||
|
|
||||||
1. Solo pueden iniciar sesión usuarios activos registrados en `glm_hub_authorized_users` y con correo del dominio `@gomezleemarketing.com`.
|
1. Todo usuario autenticado con correo `@gomezleemarketing.com` puede entrar al Hub como Usuario; `glm_hub_authorized_users` se utiliza únicamente para administradores y bloqueos excepcionales.
|
||||||
2. Los roles válidos son `admin` y `member`.
|
2. Los roles válidos son `admin` y `member`.
|
||||||
3. Los administradores autorizados son:
|
3. Los administradores autorizados son:
|
||||||
- José Leopoldo Gómez — `jgomez@gomezleemarketing.com`
|
- José Leopoldo Gómez — `jgomez@gomezleemarketing.com`
|
||||||
@@ -318,7 +318,7 @@ Si se utiliza un VirtualHost local, registrar también su URL completa.
|
|||||||
|
|
||||||
1. El usuario entra al Hub y selecciona **Continuar con Google**.
|
1. El usuario entra al Hub y selecciona **Continuar con Google**.
|
||||||
2. Supabase Auth valida la cuenta de Google.
|
2. Supabase Auth valida la cuenta de Google.
|
||||||
3. El frontend consulta `glm_hub_authorized_users` para confirmar que el usuario esté activo y conocer su rol.
|
3. El frontend permite automáticamente el acceso al dominio corporativo y consulta `glm_hub_authorized_users` solo para detectar administradores o bloqueos excepcionales.
|
||||||
4. El catálogo compartido se carga desde `glm_hub_apps`.
|
4. El catálogo compartido se carga desde `glm_hub_apps`.
|
||||||
5. Los favoritos y recientes se cargan para el usuario autenticado.
|
5. Los favoritos y recientes se cargan para el usuario autenticado.
|
||||||
6. Supabase Realtime mantiene el catálogo actualizado en sesiones abiertas.
|
6. Supabase Realtime mantiene el catálogo actualizado en sesiones abiertas.
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- GLM HUB — MIGRACIÓN: ACCESO AUTOMÁTICO POR DOMINIO
|
||||||
|
-- ============================================================================
|
||||||
|
-- Ejecutar una sola vez sobre la base existente.
|
||||||
|
-- Resultado:
|
||||||
|
-- * Todo correo @gomezleemarketing.com entra automáticamente como Usuario.
|
||||||
|
-- * glm_hub_authorized_users conserva solo administradores y bloqueos.
|
||||||
|
-- * Los permisos internos de cada aplicación continúan fuera del Hub.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
comment on table public.glm_hub_authorized_users is
|
||||||
|
'Excepciones de acceso del GLM Hub: administradores activos y usuarios bloqueados. Los usuarios corporativos normales no requieren registro.';
|
||||||
|
|
||||||
|
-- Permite que cada persona consulte únicamente su propia excepción, incluyendo
|
||||||
|
-- un posible bloqueo con is_active = false.
|
||||||
|
drop policy if exists "GLM Hub users can read only their active access"
|
||||||
|
on public.glm_hub_authorized_users;
|
||||||
|
drop policy if exists "GLM Hub users can read their own control record"
|
||||||
|
on public.glm_hub_authorized_users;
|
||||||
|
|
||||||
|
create policy "GLM Hub users can read their own control record"
|
||||||
|
on public.glm_hub_authorized_users
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
email = lower(coalesce((select auth.jwt() ->> 'email'), ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Garantiza los cinco administradores definidos.
|
||||||
|
insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||||
|
values
|
||||||
|
('jgomez@gomezleemarketing.com', 'José Leopoldo Gómez', 'admin', true),
|
||||||
|
('iaracena@gomezleemarketing.com', 'Isaac Aracena', 'admin', true),
|
||||||
|
('ethen@gomezleemarketing.com', 'Eidan Then', 'admin', true),
|
||||||
|
('mgomez@gomezleemarketing.com', 'Máximo Gomez', 'admin', true),
|
||||||
|
('lmatos@gomezleemarketing.com', 'Luis Matos', 'admin', true)
|
||||||
|
on conflict (email) do update
|
||||||
|
set
|
||||||
|
full_name = excluded.full_name,
|
||||||
|
role = excluded.role,
|
||||||
|
is_active = true,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
-- Los usuarios normales ya no requieren una fila. Se conservan administradores
|
||||||
|
-- y bloqueos (member con is_active = false).
|
||||||
|
delete from public.glm_hub_authorized_users
|
||||||
|
where role = 'member'
|
||||||
|
and is_active = true;
|
||||||
|
|
||||||
|
create or replace function public.glm_hub_is_active_user()
|
||||||
|
returns boolean
|
||||||
|
language plpgsql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_email text;
|
||||||
|
begin
|
||||||
|
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
|
||||||
|
|
||||||
|
return
|
||||||
|
auth.uid() is not null
|
||||||
|
and v_email ~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$'
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from public.glm_hub_authorized_users as access
|
||||||
|
where access.email = v_email
|
||||||
|
and access.is_active = false
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function public.glm_hub_is_admin()
|
||||||
|
returns boolean
|
||||||
|
language plpgsql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_email text;
|
||||||
|
begin
|
||||||
|
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
|
||||||
|
|
||||||
|
return
|
||||||
|
auth.uid() is not null
|
||||||
|
and exists (
|
||||||
|
select 1
|
||||||
|
from public.glm_hub_authorized_users as access
|
||||||
|
where access.email = v_email
|
||||||
|
and access.role = 'admin'
|
||||||
|
and access.is_active = true
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.glm_hub_is_active_user() owner to postgres;
|
||||||
|
alter function public.glm_hub_is_admin() owner to postgres;
|
||||||
|
|
||||||
|
revoke all on function public.glm_hub_is_active_user() from public, anon;
|
||||||
|
revoke all on function public.glm_hub_is_admin() from public, anon;
|
||||||
|
grant execute on function public.glm_hub_is_active_user() to authenticated;
|
||||||
|
grant execute on function public.glm_hub_is_admin() to authenticated;
|
||||||
|
grant execute on function public.glm_hub_is_active_user() to service_role;
|
||||||
|
grant execute on function public.glm_hub_is_admin() to service_role;
|
||||||
|
|
||||||
|
-- Actualiza la solicitud de acceso para que un usuario corporativo no necesite
|
||||||
|
-- existir previamente en glm_hub_authorized_users.
|
||||||
|
create or replace function public.glm_hub_prepare_access_request(p_app_id uuid)
|
||||||
|
returns jsonb
|
||||||
|
language plpgsql
|
||||||
|
volatile
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_user_id uuid;
|
||||||
|
v_email text;
|
||||||
|
v_name text;
|
||||||
|
v_control_name text;
|
||||||
|
v_role text;
|
||||||
|
v_is_active boolean;
|
||||||
|
v_app_name text;
|
||||||
|
v_app_category text;
|
||||||
|
v_request_id uuid;
|
||||||
|
begin
|
||||||
|
v_user_id := auth.uid();
|
||||||
|
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
|
||||||
|
|
||||||
|
if v_user_id is null or v_email = '' then
|
||||||
|
return jsonb_build_object('ok', false, 'error', 'Tu sesión no es válida. Cierra sesión e inicia nuevamente.');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_email !~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$' then
|
||||||
|
return jsonb_build_object('ok', false, 'error', 'Debes utilizar una cuenta corporativa de GomezLee Marketing.');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select access.full_name, access.role, access.is_active
|
||||||
|
into v_control_name, v_role, v_is_active
|
||||||
|
from public.glm_hub_authorized_users as access
|
||||||
|
where access.email = v_email
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if found and v_is_active = false then
|
||||||
|
return jsonb_build_object('ok', false, 'error', 'Tu acceso al GLM Hub está deshabilitado. Contacta a IT Support.');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if coalesce(v_role, 'member') = 'admin' then
|
||||||
|
return jsonb_build_object('ok', false, 'error', 'Los administradores no necesitan solicitar acceso desde el Hub.');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_name := coalesce(
|
||||||
|
nullif(btrim(v_control_name), ''),
|
||||||
|
nullif(btrim(auth.jwt() -> 'user_metadata' ->> 'full_name'), ''),
|
||||||
|
nullif(btrim(auth.jwt() -> 'user_metadata' ->> 'name'), ''),
|
||||||
|
split_part(v_email, '@', 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
select app.name, app.category
|
||||||
|
into v_app_name, v_app_category
|
||||||
|
from public.glm_hub_apps as app
|
||||||
|
where app.id = p_app_id
|
||||||
|
and app.visibility = 'published'
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if not found then
|
||||||
|
return jsonb_build_object('ok', false, 'error', 'La aplicación seleccionada ya no está publicada.');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from public.glm_hub_access_requests as request
|
||||||
|
where request.requester_email = v_email
|
||||||
|
and request.app_id = p_app_id
|
||||||
|
and request.status = 'pending'
|
||||||
|
) then
|
||||||
|
return jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'PENDING_EXISTS',
|
||||||
|
'error', 'Ya existe una solicitud pendiente para esta aplicación.'
|
||||||
|
);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into public.glm_hub_access_requests (
|
||||||
|
requester_user_id,
|
||||||
|
requester_name,
|
||||||
|
requester_email,
|
||||||
|
app_id,
|
||||||
|
app_name,
|
||||||
|
app_category
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
v_user_id,
|
||||||
|
v_name,
|
||||||
|
v_email,
|
||||||
|
p_app_id,
|
||||||
|
v_app_name,
|
||||||
|
v_app_category
|
||||||
|
)
|
||||||
|
returning id into v_request_id;
|
||||||
|
|
||||||
|
return jsonb_build_object(
|
||||||
|
'ok', true,
|
||||||
|
'requestId', v_request_id,
|
||||||
|
'requesterName', v_name,
|
||||||
|
'requesterEmail', v_email,
|
||||||
|
'appName', v_app_name,
|
||||||
|
'appCategory', v_app_category
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
alter function public.glm_hub_prepare_access_request(uuid) owner to postgres;
|
||||||
|
revoke all on function public.glm_hub_prepare_access_request(uuid) from public, anon;
|
||||||
|
grant execute on function public.glm_hub_prepare_access_request(uuid) to authenticated;
|
||||||
|
grant execute on function public.glm_hub_prepare_access_request(uuid) to service_role;
|
||||||
|
|
||||||
|
notify pgrst, 'reload schema';
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
-- ==========================================================================
|
||||||
|
-- OPERACIONES FUTURAS
|
||||||
|
-- ==========================================================================
|
||||||
|
|
||||||
|
-- HACER ADMINISTRADOR:
|
||||||
|
-- insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||||
|
-- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'admin', true)
|
||||||
|
-- on conflict (email) do update
|
||||||
|
-- set full_name = excluded.full_name, role = 'admin', is_active = true, updated_at = now();
|
||||||
|
|
||||||
|
-- DEVOLVER A USUARIO NORMAL:
|
||||||
|
-- delete from public.glm_hub_authorized_users
|
||||||
|
-- where email = 'usuario@gomezleemarketing.com';
|
||||||
|
|
||||||
|
-- BLOQUEAR USUARIO:
|
||||||
|
-- insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||||
|
-- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'member', false)
|
||||||
|
-- on conflict (email) do update
|
||||||
|
-- set full_name = excluded.full_name, role = 'member', is_active = false, updated_at = now();
|
||||||
|
|
||||||
|
-- DESBLOQUEAR USUARIO:
|
||||||
|
-- delete from public.glm_hub_authorized_users
|
||||||
|
-- where email = 'usuario@gomezleemarketing.com'
|
||||||
|
-- and role = 'member';
|
||||||
+104
-44
@@ -2,7 +2,7 @@
|
|||||||
-- GLM HUB — ESQUEMA COMPLETO DE SUPABASE
|
-- GLM HUB — ESQUEMA COMPLETO DE SUPABASE
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- Incluye:
|
-- Incluye:
|
||||||
-- 1) Usuarios autorizados y roles.
|
-- 1) Acceso automático por dominio, administradores y bloqueos excepcionales.
|
||||||
-- 2) Catálogo compartido para todos los usuarios del Hub.
|
-- 2) Catálogo compartido para todos los usuarios del Hub.
|
||||||
-- 3) Favoritos y accesos recientes por usuario.
|
-- 3) Favoritos y accesos recientes por usuario.
|
||||||
-- 4) Bucket público para los íconos.
|
-- 4) Bucket público para los íconos.
|
||||||
@@ -20,8 +20,13 @@ begin;
|
|||||||
create extension if not exists pgcrypto with schema extensions;
|
create extension if not exists pgcrypto with schema extensions;
|
||||||
|
|
||||||
-- --------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------
|
||||||
-- 1. USUARIOS AUTORIZADOS
|
-- 1. ACCESO POR DOMINIO, ADMINISTRADORES Y BLOQUEOS
|
||||||
-- --------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------
|
||||||
|
-- Regla vigente:
|
||||||
|
-- * Todo usuario autenticado con @gomezleemarketing.com entra como member.
|
||||||
|
-- * Un registro activo con role = 'admin' concede funciones administrativas.
|
||||||
|
-- * Un registro con is_active = false bloquea excepcionalmente ese correo.
|
||||||
|
-- * Los usuarios normales no necesitan una fila en esta tabla.
|
||||||
|
|
||||||
create table if not exists public.glm_hub_authorized_users (
|
create table if not exists public.glm_hub_authorized_users (
|
||||||
id uuid primary key default gen_random_uuid(),
|
id uuid primary key default gen_random_uuid(),
|
||||||
@@ -54,6 +59,9 @@ create table if not exists public.glm_hub_authorized_users (
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
comment on table public.glm_hub_authorized_users is
|
||||||
|
'Excepciones de acceso del GLM Hub: administradores activos y usuarios bloqueados. Los usuarios corporativos normales no requieren registro.';
|
||||||
|
|
||||||
create or replace function public.glm_hub_normalize_authorized_user()
|
create or replace function public.glm_hub_normalize_authorized_user()
|
||||||
returns trigger
|
returns trigger
|
||||||
language plpgsql
|
language plpgsql
|
||||||
@@ -85,14 +93,17 @@ grant all on table public.glm_hub_authorized_users to service_role;
|
|||||||
|
|
||||||
drop policy if exists "GLM Hub users can read only their active access"
|
drop policy if exists "GLM Hub users can read only their active access"
|
||||||
on public.glm_hub_authorized_users;
|
on public.glm_hub_authorized_users;
|
||||||
|
drop policy if exists "GLM Hub users can read their own control record"
|
||||||
|
on public.glm_hub_authorized_users;
|
||||||
|
|
||||||
create policy "GLM Hub users can read only their active access"
|
-- Cada persona solo puede consultar su propia excepción. Esto permite que el
|
||||||
|
-- frontend detecte también un bloqueo (is_active = false) sin exponer a otros.
|
||||||
|
create policy "GLM Hub users can read their own control record"
|
||||||
on public.glm_hub_authorized_users
|
on public.glm_hub_authorized_users
|
||||||
for select
|
for select
|
||||||
to authenticated
|
to authenticated
|
||||||
using (
|
using (
|
||||||
is_active = true
|
email = lower(coalesce((select auth.jwt() ->> 'email'), ''))
|
||||||
and email = lower(coalesce((select auth.jwt() ->> 'email'), ''))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||||
@@ -109,41 +120,64 @@ set
|
|||||||
is_active = true,
|
is_active = true,
|
||||||
updated_at = now();
|
updated_at = now();
|
||||||
|
|
||||||
-- Estas funciones se ejecutan con los permisos/RLS del usuario autenticado.
|
-- Los registros member activos ya no son necesarios: la ausencia de fila
|
||||||
|
-- significa Usuario normal. Se conservan únicamente admins y bloqueos.
|
||||||
|
delete from public.glm_hub_authorized_users
|
||||||
|
where role = 'member'
|
||||||
|
and is_active = true;
|
||||||
|
|
||||||
create or replace function public.glm_hub_is_active_user()
|
create or replace function public.glm_hub_is_active_user()
|
||||||
returns boolean
|
returns boolean
|
||||||
language sql
|
language plpgsql
|
||||||
stable
|
stable
|
||||||
security invoker
|
security definer
|
||||||
set search_path = public, auth
|
set search_path = ''
|
||||||
as $$
|
as $$
|
||||||
select exists (
|
declare
|
||||||
select 1
|
v_email text;
|
||||||
from public.glm_hub_authorized_users as access
|
begin
|
||||||
where access.is_active = true
|
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
|
||||||
and access.email = lower(coalesce((select auth.jwt() ->> 'email'), ''))
|
|
||||||
);
|
return
|
||||||
|
auth.uid() is not null
|
||||||
|
and v_email ~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$'
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from public.glm_hub_authorized_users as access
|
||||||
|
where access.email = v_email
|
||||||
|
and access.is_active = false
|
||||||
|
);
|
||||||
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
create or replace function public.glm_hub_is_admin()
|
create or replace function public.glm_hub_is_admin()
|
||||||
returns boolean
|
returns boolean
|
||||||
language sql
|
language plpgsql
|
||||||
stable
|
stable
|
||||||
security invoker
|
security definer
|
||||||
set search_path = public, auth
|
set search_path = ''
|
||||||
as $$
|
as $$
|
||||||
select exists (
|
declare
|
||||||
select 1
|
v_email text;
|
||||||
from public.glm_hub_authorized_users as access
|
begin
|
||||||
where access.is_active = true
|
v_email := lower(coalesce(auth.jwt() ->> 'email', ''));
|
||||||
and access.role = 'admin'
|
|
||||||
and access.email = lower(coalesce((select auth.jwt() ->> 'email'), ''))
|
return
|
||||||
);
|
auth.uid() is not null
|
||||||
|
and exists (
|
||||||
|
select 1
|
||||||
|
from public.glm_hub_authorized_users as access
|
||||||
|
where access.email = v_email
|
||||||
|
and access.role = 'admin'
|
||||||
|
and access.is_active = true
|
||||||
|
);
|
||||||
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
alter function public.glm_hub_is_active_user() owner to postgres;
|
||||||
|
alter function public.glm_hub_is_admin() owner to postgres;
|
||||||
|
|
||||||
-- n8n llama esta función usando el JWT recibido desde el frontend.
|
-- n8n llama esta función usando el JWT recibido desde el frontend.
|
||||||
-- SECURITY DEFINER permite consultar la tabla interna sin quedar bloqueado por RLS,
|
|
||||||
-- pero el correo y el usuario siempre se toman del JWT real de Supabase.
|
|
||||||
create or replace function public.glm_hub_icon_generation_access()
|
create or replace function public.glm_hub_icon_generation_access()
|
||||||
returns jsonb
|
returns jsonb
|
||||||
language plpgsql
|
language plpgsql
|
||||||
@@ -162,7 +196,7 @@ begin
|
|||||||
select exists (
|
select exists (
|
||||||
select 1
|
select 1
|
||||||
from public.glm_hub_authorized_users as access
|
from public.glm_hub_authorized_users as access
|
||||||
where lower(access.email) = v_email
|
where access.email = v_email
|
||||||
and access.role = 'admin'
|
and access.role = 'admin'
|
||||||
and access.is_active = true
|
and access.is_active = true
|
||||||
)
|
)
|
||||||
@@ -564,7 +598,9 @@ declare
|
|||||||
v_user_id uuid;
|
v_user_id uuid;
|
||||||
v_email text;
|
v_email text;
|
||||||
v_name text;
|
v_name text;
|
||||||
|
v_control_name text;
|
||||||
v_role text;
|
v_role text;
|
||||||
|
v_is_active boolean;
|
||||||
v_app_name text;
|
v_app_name text;
|
||||||
v_app_category text;
|
v_app_category text;
|
||||||
v_request_id uuid;
|
v_request_id uuid;
|
||||||
@@ -576,21 +612,31 @@ begin
|
|||||||
return jsonb_build_object('ok', false, 'error', 'Tu sesión no es válida. Cierra sesión e inicia nuevamente.');
|
return jsonb_build_object('ok', false, 'error', 'Tu sesión no es válida. Cierra sesión e inicia nuevamente.');
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
select coalesce(nullif(btrim(access.full_name), ''), split_part(v_email, '@', 1)), access.role
|
if v_email !~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$' then
|
||||||
into v_name, v_role
|
return jsonb_build_object('ok', false, 'error', 'Debes utilizar una cuenta corporativa de GomezLee Marketing.');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select access.full_name, access.role, access.is_active
|
||||||
|
into v_control_name, v_role, v_is_active
|
||||||
from public.glm_hub_authorized_users as access
|
from public.glm_hub_authorized_users as access
|
||||||
where access.email = v_email
|
where access.email = v_email
|
||||||
and access.is_active = true
|
|
||||||
limit 1;
|
limit 1;
|
||||||
|
|
||||||
if not found then
|
if found and v_is_active = false then
|
||||||
return jsonb_build_object('ok', false, 'error', 'El usuario no está autorizado en GLM Hub.');
|
return jsonb_build_object('ok', false, 'error', 'Tu acceso al GLM Hub está deshabilitado. Contacta a IT Support.');
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
if v_role = 'admin' then
|
if coalesce(v_role, 'member') = 'admin' then
|
||||||
return jsonb_build_object('ok', false, 'error', 'Los administradores no necesitan solicitar acceso desde el Hub.');
|
return jsonb_build_object('ok', false, 'error', 'Los administradores no necesitan solicitar acceso desde el Hub.');
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
|
v_name := coalesce(
|
||||||
|
nullif(btrim(v_control_name), ''),
|
||||||
|
nullif(btrim(auth.jwt() -> 'user_metadata' ->> 'full_name'), ''),
|
||||||
|
nullif(btrim(auth.jwt() -> 'user_metadata' ->> 'name'), ''),
|
||||||
|
split_part(v_email, '@', 1)
|
||||||
|
);
|
||||||
|
|
||||||
select app.name, app.category
|
select app.name, app.category
|
||||||
into v_app_name, v_app_category
|
into v_app_name, v_app_category
|
||||||
from public.glm_hub_apps as app
|
from public.glm_hub_apps as app
|
||||||
@@ -867,21 +913,35 @@ commit;
|
|||||||
-- OPERACIONES DE ADMINISTRACIÓN (EJECUTAR DESDE SQL EDITOR)
|
-- OPERACIONES DE ADMINISTRACIÓN (EJECUTAR DESDE SQL EDITOR)
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|
||||||
-- DAR ACCESO A UN USUARIO NORMAL:
|
-- IMPORTANTE:
|
||||||
|
-- Todo correo @gomezleemarketing.com entra automáticamente como Usuario.
|
||||||
|
-- La tabla glm_hub_authorized_users se usa solo para administradores y bloqueos.
|
||||||
|
|
||||||
|
-- CONVERTIR UN USUARIO EN ADMINISTRADOR:
|
||||||
-- insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
-- insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||||
-- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'member', true)
|
-- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'admin', true)
|
||||||
-- on conflict (email) do update
|
-- on conflict (email) do update
|
||||||
-- set full_name = excluded.full_name, role = 'member', is_active = true;
|
-- set full_name = excluded.full_name, role = 'admin', is_active = true, updated_at = now();
|
||||||
|
|
||||||
-- QUITAR ACCESO SIN BORRAR EL REGISTRO:
|
-- DEVOLVER UN ADMINISTRADOR A USUARIO NORMAL:
|
||||||
-- update public.glm_hub_authorized_users
|
-- delete from public.glm_hub_authorized_users
|
||||||
-- set is_active = false
|
|
||||||
-- where email = 'usuario@gomezleemarketing.com';
|
-- where email = 'usuario@gomezleemarketing.com';
|
||||||
|
|
||||||
-- REACTIVAR ACCESO:
|
-- BLOQUEAR EXCEPCIONALMENTE A UN USUARIO DEL DOMINIO:
|
||||||
-- update public.glm_hub_authorized_users
|
-- insert into public.glm_hub_authorized_users (email, full_name, role, is_active)
|
||||||
-- set is_active = true
|
-- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'member', false)
|
||||||
-- where email = 'usuario@gomezleemarketing.com';
|
-- on conflict (email) do update
|
||||||
|
-- set full_name = excluded.full_name, role = 'member', is_active = false, updated_at = now();
|
||||||
|
|
||||||
|
-- DESBLOQUEAR A UN USUARIO NORMAL:
|
||||||
|
-- delete from public.glm_hub_authorized_users
|
||||||
|
-- where email = 'usuario@gomezleemarketing.com'
|
||||||
|
-- and role = 'member';
|
||||||
|
|
||||||
|
-- VER ADMINISTRADORES Y BLOQUEOS:
|
||||||
|
-- select email, full_name, role, is_active, updated_at
|
||||||
|
-- from public.glm_hub_authorized_users
|
||||||
|
-- order by role, email;
|
||||||
|
|
||||||
-- VER SOLICITUDES DE ACCESO:
|
-- VER SOLICITUDES DE ACCESO:
|
||||||
-- select requester_name, requester_email, app_name, status, decided_by_name, decided_by_email, created_at, decided_at
|
-- select requester_name, requester_email, app_name, status, decided_by_name, decided_by_email, created_at, decided_at
|
||||||
|
|||||||
+10
-10
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
<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="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" />
|
<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>
|
<title>GLM Hub — GomezLee Marketing</title>
|
||||||
<script type="module" crossorigin src="/assets/index-B0NLzVcn.js"></script>
|
<script type="module" crossorigin src="/assets/index-_vT1KbDN.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-9ByAKvcq.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-9ByAKvcq.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+22
-11
@@ -82,6 +82,7 @@ interface AuthorizedUserRow {
|
|||||||
email: string;
|
email: string;
|
||||||
full_name: string | null;
|
full_name: string | null;
|
||||||
role: "admin" | "member";
|
role: "admin" | "member";
|
||||||
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthorizationResult {
|
interface AuthorizationResult {
|
||||||
@@ -97,11 +98,20 @@ function getOAuthRedirectUrl(): string {
|
|||||||
return new URL(import.meta.env.BASE_URL, window.location.origin).toString();
|
return new URL(import.meta.env.BASE_URL, window.location.origin).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAuthenticatedName(authSession: SupabaseAuthSession, access: AuthorizedUserRow): string {
|
function getAuthenticatedName(
|
||||||
|
authSession: SupabaseAuthSession,
|
||||||
|
access: AuthorizedUserRow | null,
|
||||||
|
): string {
|
||||||
const metadata = authSession.user.user_metadata ?? {};
|
const metadata = authSession.user.user_metadata ?? {};
|
||||||
const metadataName = metadata.full_name ?? metadata.name;
|
const metadataName = metadata.full_name ?? metadata.name;
|
||||||
const emailName = access.email.split("@")[0] || "Usuario GLM";
|
const email = authSession.user.email?.trim().toLocaleLowerCase("en-US") ?? "";
|
||||||
return access.full_name?.trim() || (typeof metadataName === "string" ? metadataName.trim() : "") || emailName;
|
const emailName = email.split("@")[0] || "Usuario GLM";
|
||||||
|
|
||||||
|
return (
|
||||||
|
access?.full_name?.trim() ||
|
||||||
|
(typeof metadataName === "string" ? metadataName.trim() : "") ||
|
||||||
|
emailName
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAuthenticatedAvatarUrl(authSession: SupabaseAuthSession): string {
|
function getAuthenticatedAvatarUrl(authSession: SupabaseAuthSession): string {
|
||||||
@@ -127,26 +137,27 @@ async function authorizeSupabaseSession(authSession: SupabaseAuthSession): Promi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Todos los correos corporativos entran como Usuario por defecto.
|
||||||
|
// La tabla solo conserva administradores y bloqueos excepcionales.
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("glm_hub_authorized_users")
|
.from("glm_hub_authorized_users")
|
||||||
.select("email, full_name, role")
|
.select("email, full_name, role, is_active")
|
||||||
.eq("email", email)
|
.eq("email", email)
|
||||||
.eq("is_active", true)
|
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
const access = data as AuthorizedUserRow | null;
|
const access = data as AuthorizedUserRow | null;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("No se pudo validar el acceso en Supabase:", error);
|
console.error("No se pudo validar el perfil de acceso:", error);
|
||||||
return {
|
return {
|
||||||
session: null,
|
session: null,
|
||||||
error: "No pudimos validar tu acceso en Supabase. Intenta nuevamente.",
|
error: "No pudimos completar el inicio de sesión. Intenta nuevamente.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!access) {
|
if (access && !access.is_active) {
|
||||||
return {
|
return {
|
||||||
session: null,
|
session: null,
|
||||||
error: "Tu correo no está autorizado para entrar al GLM Hub.",
|
error: "Tu acceso al GLM Hub está deshabilitado. Contacta a IT Support.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +166,8 @@ async function authorizeSupabaseSession(authSession: SupabaseAuthSession): Promi
|
|||||||
user: {
|
user: {
|
||||||
id: authSession.user.id,
|
id: authSession.user.id,
|
||||||
name: getAuthenticatedName(authSession, access),
|
name: getAuthenticatedName(authSession, access),
|
||||||
email: access.email,
|
email,
|
||||||
role: access.role,
|
role: access?.role === "admin" ? "admin" : "member",
|
||||||
avatarUrl: getAuthenticatedAvatarUrl(authSession),
|
avatarUrl: getAuthenticatedAvatarUrl(authSession),
|
||||||
},
|
},
|
||||||
expiresAt: (authSession.expires_at ?? Math.floor(Date.now() / 1000) + 3600) * 1000,
|
expiresAt: (authSession.expires_at ?? Math.floor(Date.now() / 1000) + 3600) * 1000,
|
||||||
|
|||||||
Reference in New Issue
Block a user