-- ============================================================================ -- GLM HUB — ESQUEMA COMPLETO DE SUPABASE -- ============================================================================ -- Incluye: -- 1) Usuarios autorizados y roles. -- 2) Catálogo compartido para todos los usuarios del Hub. -- 3) Favoritos y accesos recientes por usuario. -- 4) Bucket público para los íconos. -- 5) RLS, permisos y Realtime. -- 6) Función segura que usa n8n para validar al administrador. -- 7) Solicitudes de acceso, aprobadores y decisión única. -- -- Ejecutar el archivo completo una sola vez en: -- Supabase Studio > SQL Editor > New query > Run. -- El script es idempotente y puede volver a ejecutarse para restaurar políticas. -- ============================================================================ begin; create extension if not exists pgcrypto with schema extensions; -- -------------------------------------------------------------------------- -- 1. USUARIOS AUTORIZADOS -- -------------------------------------------------------------------------- create table if not exists public.glm_hub_authorized_users ( id uuid primary key default gen_random_uuid(), email text not null unique, full_name text, role text not null default 'member', is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint glm_hub_authorized_users_role_check check (role in ('admin', 'member')), constraint glm_hub_authorized_users_email_normalized_check check (email = lower(btrim(email))), constraint glm_hub_authorized_users_domain_check check (email ~ '^[a-z0-9.!#$%&''*+/=?^_`{|}~-]+@gomezleemarketing\.com$'), constraint glm_hub_authorized_users_admin_check check ( role <> 'admin' or email in ( 'jgomez@gomezleemarketing.com', 'iaracena@gomezleemarketing.com', 'ethen@gomezleemarketing.com', 'mgomez@gomezleemarketing.com', 'lmatos@gomezleemarketing.com' ) ) ); create or replace function public.glm_hub_normalize_authorized_user() returns trigger language plpgsql set search_path = public as $$ begin new.email := lower(btrim(new.email)); new.full_name := nullif(btrim(new.full_name), ''); new.updated_at := now(); return new; end; $$; drop trigger if exists glm_hub_normalize_authorized_user_trigger on public.glm_hub_authorized_users; create trigger glm_hub_normalize_authorized_user_trigger before insert or update on public.glm_hub_authorized_users for each row execute function public.glm_hub_normalize_authorized_user(); alter table public.glm_hub_authorized_users enable row level security; alter table public.glm_hub_authorized_users force row level security; revoke all on table public.glm_hub_authorized_users from anon; revoke all on table public.glm_hub_authorized_users from authenticated; grant select on table public.glm_hub_authorized_users to authenticated; grant all on table public.glm_hub_authorized_users to service_role; drop policy if exists "GLM Hub users can read only their active access" on public.glm_hub_authorized_users; create policy "GLM Hub users can read only their active access" on public.glm_hub_authorized_users for select to authenticated using ( is_active = true and email = lower(coalesce((select auth.jwt() ->> 'email'), '')) ); insert into public.glm_hub_authorized_users (email, full_name, role, is_active) values ('jgomez@gomezleemarketing.com', 'José Leopoldo Gómez', 'admin', true), ('iaracena@gomezleemarketing.com', 'Isaac Aracena', 'admin', true), ('ethen@gomezleemarketing.com', 'Eidan Then', 'admin', true), ('mgomez@gomezleemarketing.com', 'Máximo Gomez', 'admin', true), ('lmatos@gomezleemarketing.com', 'Luis Matos', 'admin', true) on conflict (email) do update set full_name = excluded.full_name, role = excluded.role, is_active = true, updated_at = now(); -- Estas funciones se ejecutan con los permisos/RLS del usuario autenticado. create or replace function public.glm_hub_is_active_user() returns boolean language sql stable security invoker set search_path = public, auth as $$ select exists ( select 1 from public.glm_hub_authorized_users as access where access.is_active = true and access.email = lower(coalesce((select auth.jwt() ->> 'email'), '')) ); $$; create or replace function public.glm_hub_is_admin() returns boolean language sql stable security invoker set search_path = public, auth as $$ select exists ( select 1 from public.glm_hub_authorized_users as access where access.is_active = true and access.role = 'admin' and access.email = lower(coalesce((select auth.jwt() ->> 'email'), '')) ); $$; -- 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() returns jsonb language plpgsql stable security definer set search_path = '' as $$ declare v_email text; v_user_id text; v_authorized boolean; begin v_email := lower(coalesce(auth.jwt() ->> 'email', '')); v_user_id := coalesce(auth.uid()::text, ''); select exists ( select 1 from public.glm_hub_authorized_users as access where lower(access.email) = v_email and access.role = 'admin' and access.is_active = true ) into v_authorized; return jsonb_build_object( 'authorized', coalesce(v_authorized, false), 'email', v_email, 'userId', v_user_id ); end; $$; alter function public.glm_hub_icon_generation_access() owner to postgres; revoke all on function public.glm_hub_is_active_user() from public, anon; revoke all on function public.glm_hub_is_admin() from public, anon; revoke all on function public.glm_hub_icon_generation_access() from public, anon; grant execute on function public.glm_hub_is_active_user() to authenticated; grant execute on function public.glm_hub_is_admin() to authenticated; grant execute on function public.glm_hub_icon_generation_access() to authenticated; grant execute on function public.glm_hub_is_active_user() to service_role; grant execute on function public.glm_hub_is_admin() to service_role; grant execute on function public.glm_hub_icon_generation_access() to service_role; -- -------------------------------------------------------------------------- -- 2. CATÁLOGO COMPARTIDO -- -------------------------------------------------------------------------- create table if not exists public.glm_hub_apps ( id uuid primary key default gen_random_uuid(), name text not null, description text not null, category text not null, url text, icon_url text not null, visibility text not null default 'published', created_by uuid default auth.uid() references auth.users(id) on delete set null, updated_by uuid default auth.uid() references auth.users(id) on delete set null, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint glm_hub_apps_name_length_check check (char_length(btrim(name)) between 2 and 60), constraint glm_hub_apps_description_length_check check (char_length(btrim(description)) between 1 and 180), constraint glm_hub_apps_category_check check (category in ('Administración', 'Recursos Humanos', 'CDC')), constraint glm_hub_apps_visibility_check check (visibility in ('published', 'hidden')), constraint glm_hub_apps_icon_url_check check (icon_url ~ '^https://') ); create unique index if not exists glm_hub_apps_name_unique_ci on public.glm_hub_apps (lower(btrim(name))); create index if not exists glm_hub_apps_visibility_category_name_idx on public.glm_hub_apps (visibility, category, name); create or replace function public.glm_hub_normalize_app() returns trigger language plpgsql set search_path = public as $$ begin new.name := btrim(new.name); new.description := btrim(new.description); new.category := btrim(new.category); new.url := nullif(btrim(new.url), ''); new.icon_url := btrim(new.icon_url); new.updated_at := now(); return new; end; $$; drop trigger if exists glm_hub_normalize_app_trigger on public.glm_hub_apps; create trigger glm_hub_normalize_app_trigger before insert or update on public.glm_hub_apps for each row execute function public.glm_hub_normalize_app(); alter table public.glm_hub_apps enable row level security; alter table public.glm_hub_apps force row level security; alter table public.glm_hub_apps replica identity full; revoke all on table public.glm_hub_apps from anon; revoke all on table public.glm_hub_apps from authenticated; grant select, insert, update, delete on table public.glm_hub_apps to authenticated; grant all on table public.glm_hub_apps to service_role; drop policy if exists "GLM Hub active users read shared apps" on public.glm_hub_apps; drop policy if exists "GLM Hub admins create apps" on public.glm_hub_apps; drop policy if exists "GLM Hub admins update apps" on public.glm_hub_apps; drop policy if exists "GLM Hub admins delete apps" on public.glm_hub_apps; create policy "GLM Hub active users read shared apps" on public.glm_hub_apps for select to authenticated using ( public.glm_hub_is_active_user() and (visibility = 'published' or public.glm_hub_is_admin()) ); create policy "GLM Hub admins create apps" on public.glm_hub_apps for insert to authenticated with check ( public.glm_hub_is_admin() and created_by = (select auth.uid()) and updated_by = (select auth.uid()) ); create policy "GLM Hub admins update apps" on public.glm_hub_apps for update to authenticated using (public.glm_hub_is_admin()) with check ( public.glm_hub_is_admin() and updated_by = (select auth.uid()) ); create policy "GLM Hub admins delete apps" on public.glm_hub_apps for delete to authenticated using (public.glm_hub_is_admin()); -- -------------------------------------------------------------------------- -- 3. FAVORITOS POR USUARIO -- -------------------------------------------------------------------------- create table if not exists public.glm_hub_favorites ( user_id uuid not null references auth.users(id) on delete cascade, app_id uuid not null references public.glm_hub_apps(id) on delete cascade, created_at timestamptz not null default now(), primary key (user_id, app_id) ); create index if not exists glm_hub_favorites_user_created_idx on public.glm_hub_favorites (user_id, created_at desc); alter table public.glm_hub_favorites enable row level security; alter table public.glm_hub_favorites force row level security; revoke all on table public.glm_hub_favorites from anon; revoke all on table public.glm_hub_favorites from authenticated; grant select, insert, update, delete on table public.glm_hub_favorites to authenticated; grant all on table public.glm_hub_favorites to service_role; drop policy if exists "GLM Hub users read their favorites" on public.glm_hub_favorites; drop policy if exists "GLM Hub users create their favorites" on public.glm_hub_favorites; drop policy if exists "GLM Hub users update their favorites" on public.glm_hub_favorites; drop policy if exists "GLM Hub users delete their favorites" on public.glm_hub_favorites; create policy "GLM Hub users read their favorites" on public.glm_hub_favorites for select to authenticated using (public.glm_hub_is_active_user() and user_id = (select auth.uid())); create policy "GLM Hub users create their favorites" on public.glm_hub_favorites for insert to authenticated with check (public.glm_hub_is_active_user() and user_id = (select auth.uid())); create policy "GLM Hub users update their favorites" on public.glm_hub_favorites for update to authenticated using (public.glm_hub_is_active_user() and user_id = (select auth.uid())) with check (public.glm_hub_is_active_user() and user_id = (select auth.uid())); create policy "GLM Hub users delete their favorites" on public.glm_hub_favorites for delete to authenticated using (public.glm_hub_is_active_user() and user_id = (select auth.uid())); -- -------------------------------------------------------------------------- -- 4. ACCESOS RECIENTES POR USUARIO -- -------------------------------------------------------------------------- create table if not exists public.glm_hub_recent_apps ( user_id uuid not null references auth.users(id) on delete cascade, app_id uuid not null references public.glm_hub_apps(id) on delete cascade, last_opened_at timestamptz not null default now(), primary key (user_id, app_id) ); create index if not exists glm_hub_recent_apps_user_date_idx on public.glm_hub_recent_apps (user_id, last_opened_at desc); alter table public.glm_hub_recent_apps enable row level security; alter table public.glm_hub_recent_apps force row level security; revoke all on table public.glm_hub_recent_apps from anon; revoke all on table public.glm_hub_recent_apps from authenticated; grant select, insert, update, delete on table public.glm_hub_recent_apps to authenticated; grant all on table public.glm_hub_recent_apps to service_role; drop policy if exists "GLM Hub users read their recent apps" on public.glm_hub_recent_apps; drop policy if exists "GLM Hub users create their recent apps" on public.glm_hub_recent_apps; drop policy if exists "GLM Hub users update their recent apps" on public.glm_hub_recent_apps; drop policy if exists "GLM Hub users delete their recent apps" on public.glm_hub_recent_apps; create policy "GLM Hub users read their recent apps" on public.glm_hub_recent_apps for select to authenticated using (public.glm_hub_is_active_user() and user_id = (select auth.uid())); create policy "GLM Hub users create their recent apps" on public.glm_hub_recent_apps for insert to authenticated with check (public.glm_hub_is_active_user() and user_id = (select auth.uid())); create policy "GLM Hub users update their recent apps" on public.glm_hub_recent_apps for update to authenticated using (public.glm_hub_is_active_user() and user_id = (select auth.uid())) with check (public.glm_hub_is_active_user() and user_id = (select auth.uid())); create policy "GLM Hub users delete their recent apps" on public.glm_hub_recent_apps for delete to authenticated using (public.glm_hub_is_active_user() and user_id = (select auth.uid())); -- -------------------------------------------------------------------------- -- 5. STORAGE PARA LOS ÍCONOS -- -------------------------------------------------------------------------- insert into storage.buckets ( id, name, public, file_size_limit, allowed_mime_types ) values ( 'glm-hub-icons', 'glm-hub-icons', true, 1048576, array['image/png', 'image/jpeg', 'image/webp']::text[] ) on conflict (id) do update set name = excluded.name, public = true, file_size_limit = excluded.file_size_limit, allowed_mime_types = excluded.allowed_mime_types; drop policy if exists "GLM Hub active users read icons" on storage.objects; drop policy if exists "GLM Hub admins upload icons" on storage.objects; drop policy if exists "GLM Hub admins update icons" on storage.objects; drop policy if exists "GLM Hub admins delete icons" on storage.objects; create policy "GLM Hub active users read icons" on storage.objects for select to authenticated using ( bucket_id = 'glm-hub-icons' and public.glm_hub_is_active_user() ); create policy "GLM Hub admins upload icons" on storage.objects for insert to authenticated with check ( bucket_id = 'glm-hub-icons' and public.glm_hub_is_admin() ); create policy "GLM Hub admins update icons" on storage.objects for update to authenticated using ( bucket_id = 'glm-hub-icons' and public.glm_hub_is_admin() ) with check ( bucket_id = 'glm-hub-icons' and public.glm_hub_is_admin() ); create policy "GLM Hub admins delete icons" on storage.objects for delete to authenticated using ( bucket_id = 'glm-hub-icons' and public.glm_hub_is_admin() ); -- -------------------------------------------------------------------------- -- 6. SOLICITUDES DE ACCESO A APLICACIONES -- -------------------------------------------------------------------------- create table if not exists public.glm_hub_access_requests ( id uuid primary key default gen_random_uuid(), requester_user_id uuid not null references auth.users(id) on delete cascade, requester_name text not null, requester_email text not null, app_id uuid references public.glm_hub_apps(id) on delete set null, app_name text not null, app_category text not null, status text not null default 'pending', decided_by_name text, decided_by_email text, decided_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint glm_hub_access_requests_status_check check (status in ('pending', 'approved', 'rejected')), constraint glm_hub_access_requests_requester_email_check check (requester_email = lower(btrim(requester_email))), constraint glm_hub_access_requests_category_check check (app_category in ('Administración', 'Recursos Humanos', 'CDC')) ); create unique index if not exists glm_hub_access_requests_one_pending_idx on public.glm_hub_access_requests (requester_email, app_id) where status = 'pending' and app_id is not null; create index if not exists glm_hub_access_requests_status_created_idx on public.glm_hub_access_requests (status, created_at desc); create table if not exists public.glm_hub_access_request_approvers ( id uuid primary key default gen_random_uuid(), request_id uuid not null references public.glm_hub_access_requests(id) on delete cascade, approver_name text not null, approver_email text not null, decision_token text not null unique, decision_token_hash text not null unique, clicked_at timestamptz, created_at timestamptz not null default now(), constraint glm_hub_access_request_approvers_email_check check ( approver_email in ( 'iaracena@gomezleemarketing.com', 'jgomez@gomezleemarketing.com', 'mgomez@gomezleemarketing.com' ) ), constraint glm_hub_access_request_approvers_unique_request_email unique (request_id, approver_email) ); alter table public.glm_hub_access_requests enable row level security; alter table public.glm_hub_access_requests force row level security; alter table public.glm_hub_access_request_approvers enable row level security; alter table public.glm_hub_access_request_approvers force row level security; revoke all on table public.glm_hub_access_requests from anon, authenticated; revoke all on table public.glm_hub_access_request_approvers from anon, authenticated; grant all on table public.glm_hub_access_requests to service_role; grant all on table public.glm_hub_access_request_approvers to service_role; -- Los usuarios pueden consultar solamente sus propias solicitudes desde Supabase, -- aunque el frontend actual no necesita leer esta tabla. drop policy if exists "GLM Hub users read their access requests" on public.glm_hub_access_requests; create policy "GLM Hub users read their access requests" on public.glm_hub_access_requests for select to authenticated using ( requester_user_id = auth.uid() and public.glm_hub_is_active_user() ); grant select on table public.glm_hub_access_requests to authenticated; -- Registra la solicitud usando exclusivamente la identidad del JWT del usuario. -- No devuelve enlaces de aprobación al navegador, evitando que el solicitante -- pueda obtenerlos o aprobarse a sí mismo. create or replace function public.glm_hub_prepare_access_request(p_app_id uuid) returns jsonb language plpgsql volatile security definer set search_path = '' as $$ declare v_user_id uuid; v_email text; v_name text; v_role text; 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; select coalesce(nullif(btrim(access.full_name), ''), split_part(v_email, '@', 1)), access.role into v_name, v_role from public.glm_hub_authorized_users as access where access.email = v_email and access.is_active = true limit 1; if not found then return jsonb_build_object('ok', false, 'error', 'El usuario no está autorizado en GLM Hub.'); end if; if v_role = 'admin' then return jsonb_build_object('ok', false, 'error', 'Los administradores no necesitan solicitar acceso desde el Hub.'); end if; select app.name, app.category into v_app_name, v_app_category from public.glm_hub_apps as app where app.id = p_app_id and app.visibility = 'published' limit 1; if not found then return jsonb_build_object('ok', false, 'error', 'La aplicación seleccionada ya no está publicada.'); end if; if exists ( select 1 from public.glm_hub_access_requests as request where request.requester_email = v_email and request.app_id = p_app_id and request.status = 'pending' ) then return jsonb_build_object( 'ok', false, 'code', 'PENDING_EXISTS', 'error', 'Ya existe una solicitud pendiente para esta aplicación.' ); end if; insert into public.glm_hub_access_requests ( requester_user_id, requester_name, requester_email, app_id, app_name, app_category ) values ( v_user_id, v_name, v_email, p_app_id, v_app_name, v_app_category ) returning id into v_request_id; return jsonb_build_object( 'ok', true, 'requestId', v_request_id, 'requesterName', v_name, 'requesterEmail', v_email, 'appName', v_app_name, 'appCategory', v_app_category ); end; $$; alter function public.glm_hub_prepare_access_request(uuid) owner to postgres; revoke all on function public.glm_hub_prepare_access_request(uuid) from public, anon; grant execute on function public.glm_hub_prepare_access_request(uuid) to authenticated; grant execute on function public.glm_hub_prepare_access_request(uuid) to service_role; -- Emite o recupera los tres enlaces de decisión. Solo n8n, usando la clave -- service_role guardada en su nodo HTTP Request, puede ejecutar esta función. create or replace function public.glm_hub_issue_access_request_tokens(p_request_id uuid) returns jsonb language plpgsql volatile security definer set search_path = '' as $$ declare v_request public.glm_hub_access_requests%rowtype; v_token_isaac text; v_token_jose text; v_token_maximo text; 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; 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'); insert into public.glm_hub_access_request_approvers ( request_id, approver_name, approver_email, decision_token, decision_token_hash ) values ( 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; 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 jsonb_agg( jsonb_build_object( 'name', approver.approver_name, 'email', approver.approver_email, 'token', approver.decision_token ) order by approver.approver_email ) from public.glm_hub_access_request_approvers as approver where approver.request_id = p_request_id ) ); end; $$; alter function public.glm_hub_issue_access_request_tokens(uuid) owner to postgres; revoke all on function public.glm_hub_issue_access_request_tokens(uuid) from public, anon, authenticated; grant execute on function public.glm_hub_issue_access_request_tokens(uuid) to service_role; -- Registra una sola decisión de forma atómica. Aunque los botones sigan visibles -- en correos ya entregados, cualquier segundo clic queda bloqueado en la base de datos. create or replace function public.glm_hub_decide_access_request( p_token text, p_decision text ) returns jsonb language plpgsql volatile security definer set search_path = '' as $$ declare v_request_id uuid; v_approver_name text; v_approver_email text; v_request public.glm_hub_access_requests%rowtype; v_normalized_decision text; begin v_normalized_decision := lower(btrim(coalesce(p_decision, ''))); if v_normalized_decision not in ('approved', 'rejected') then return jsonb_build_object('ok', false, 'error', 'La decisión recibida no es válida.'); end if; select approver.request_id, approver.approver_name, approver.approver_email into v_request_id, v_approver_name, v_approver_email from public.glm_hub_access_request_approvers as approver where approver.decision_token_hash = encode( extensions.digest(btrim(coalesce(p_token, '')), 'sha256'), 'hex' ) limit 1; if not found then return jsonb_build_object('ok', false, 'error', 'El enlace de decisión no es válido o ya no existe.'); end if; update public.glm_hub_access_requests as request set status = v_normalized_decision, decided_by_name = v_approver_name, decided_by_email = v_approver_email, decided_at = now(), updated_at = now() where request.id = v_request_id and request.status = 'pending' returning request.* into v_request; if found then update public.glm_hub_access_request_approvers set clicked_at = now() where request_id = v_request_id and approver_email = v_approver_email; return jsonb_build_object( 'ok', true, 'alreadyDecided', false, 'requestId', v_request.id, 'status', v_request.status, 'requesterName', v_request.requester_name, 'requesterEmail', v_request.requester_email, 'appName', v_request.app_name, 'appCategory', v_request.app_category, 'decidedByName', v_request.decided_by_name, 'decidedByEmail', v_request.decided_by_email, 'decidedAt', v_request.decided_at ); end if; select request.* into v_request from public.glm_hub_access_requests as request where request.id = v_request_id; return jsonb_build_object( 'ok', false, 'alreadyDecided', true, 'status', v_request.status, 'requesterName', v_request.requester_name, 'requesterEmail', v_request.requester_email, 'appName', v_request.app_name, 'decidedByName', coalesce(v_request.decided_by_name, ''), 'decidedByEmail', coalesce(v_request.decided_by_email, ''), 'decidedAt', v_request.decided_at, 'error', 'Esta solicitud ya fue atendida y no admite otra decisión.' ); end; $$; alter function public.glm_hub_decide_access_request(text, text) owner to postgres; revoke all on function public.glm_hub_decide_access_request(text, text) from public; grant execute on function public.glm_hub_decide_access_request(text, text) to anon; grant execute on function public.glm_hub_decide_access_request(text, text) to authenticated; grant execute on function public.glm_hub_decide_access_request(text, text) to service_role; -- -------------------------------------------------------------------------- -- 7. REALTIME DEL CATÁLOGO -- -------------------------------------------------------------------------- -- Hace que una aplicación creada/editada/eliminada por un administrador -- aparezca en las sesiones abiertas de los demás usuarios sin recargar. do $$ begin if exists ( select 1 from pg_publication where pubname = 'supabase_realtime' ) and not exists ( select 1 from pg_publication_tables where pubname = 'supabase_realtime' and schemaname = 'public' and tablename = 'glm_hub_apps' ) then execute 'alter publication supabase_realtime add table public.glm_hub_apps'; end if; end; $$; commit; -- ============================================================================ -- OPERACIONES DE ADMINISTRACIÓN (EJECUTAR DESDE SQL EDITOR) -- ============================================================================ -- DAR ACCESO A UN USUARIO NORMAL: -- insert into public.glm_hub_authorized_users (email, full_name, role, is_active) -- values ('usuario@gomezleemarketing.com', 'Nombre del usuario', 'member', true) -- on conflict (email) do update -- set full_name = excluded.full_name, role = 'member', is_active = true; -- QUITAR ACCESO SIN BORRAR EL REGISTRO: -- update public.glm_hub_authorized_users -- set is_active = false -- where email = 'usuario@gomezleemarketing.com'; -- REACTIVAR ACCESO: -- update public.glm_hub_authorized_users -- set is_active = true -- where email = 'usuario@gomezleemarketing.com'; -- VER SOLICITUDES DE ACCESO: -- select requester_name, requester_email, app_name, status, decided_by_name, decided_by_email, created_at, decided_at -- from public.glm_hub_access_requests -- order by created_at desc; -- VER EL CATÁLOGO COMPARTIDO: -- select id, name, category, visibility, url, icon_url, updated_at -- from public.glm_hub_apps -- order by name;