-- ============================================================================ -- GLM HUB — ESQUEMA COMPLETO DE SUPABASE -- ============================================================================ -- Incluye: -- 1) Acceso automático por dominio, administradores y bloqueos excepcionales. -- 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. 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 ( 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' ) ) ); 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() 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; drop policy if exists "GLM Hub users can read their own control record" on public.glm_hub_authorized_users; -- 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 for select to authenticated using ( 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(); -- 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() 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; -- n8n llama esta función usando el JWT recibido desde el frontend. 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 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_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; -- 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) -- ============================================================================ -- 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) -- 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 UN ADMINISTRADOR A USUARIO NORMAL: -- delete from public.glm_hub_authorized_users -- where email = 'usuario@gomezleemarketing.com'; -- BLOQUEAR EXCEPCIONALMENTE A UN USUARIO DEL DOMINIO: -- 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 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: -- 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;