Files
inq-roi-simulador-web/sprints/sprint-4/ETAPA_5_PROMPT.md

9.3 KiB
Raw Permalink Blame History

Sprint 4 — Etapa 5: Modelo de privacidad — RLS granular + UI de visibilidad

Contexto

InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind. Etapas 14 completadas: auth Google, procesos/grupos/settings/actividades/gateways/recursos/simulaciones en Supabase, Dexie eliminado.

Antes de arrancar esta etapa: commitear Etapas 3 y 4 juntas en un único commit con mensaje feat(sprint-4/etapa-3-4): migración actividades/gateways/recursos/simulaciones a Supabase + limpieza Dexie.

Esta etapa reemplaza la política RLS amplia authenticated_all_processes por un modelo granular basado en ownership y acceso explícito, e incorpora los elementos de UI correspondientes en la biblioteca.


Prerequisitos — verificar antes de empezar

  • Scripts 001009 ejecutados en Supabase Cloud
  • src/auth/AuthContext.tsx con useAuth() devolviendo { user } con al menos user.id
  • src/persistence/supabase/process-repo.ts existente

Alcance estricto — qué entra en esta etapa

1. Nuevos scripts SQL (crear en supabase/migrations/)

010_create_user_groups.sql — tablas de grupos de usuarios:

CREATE TABLE IF NOT EXISTS public.user_groups (
  id          uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
  name        text         NOT NULL,
  created_by  uuid         REFERENCES public.users(id),
  created_at  timestamptz  NOT NULL DEFAULT now()
);

ALTER TABLE public.user_groups ENABLE ROW LEVEL SECURITY;

CREATE POLICY "read_groups" ON public.user_groups
  FOR SELECT USING (auth.uid() IS NOT NULL);

CREATE POLICY "manage_groups" ON public.user_groups
  FOR ALL USING (
    EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND platform_role = 'platform_admin')
    OR created_by = auth.uid()
  );

CREATE TABLE IF NOT EXISTS public.group_memberships (
  user_id   uuid  NOT NULL REFERENCES public.users(id)  ON DELETE CASCADE,
  group_id  uuid  NOT NULL REFERENCES public.user_groups(id) ON DELETE CASCADE,
  role      text  NOT NULL DEFAULT 'member' CHECK (role IN ('owner','member')),
  PRIMARY KEY (user_id, group_id)
);

ALTER TABLE public.group_memberships ENABLE ROW LEVEL SECURITY;

CREATE POLICY "read_memberships" ON public.group_memberships
  FOR SELECT USING (auth.uid() IS NOT NULL);

CREATE POLICY "manage_memberships" ON public.group_memberships
  FOR ALL USING (
    EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND platform_role = 'platform_admin')
    OR EXISTS (
      SELECT 1 FROM public.user_groups
      WHERE id = group_memberships.group_id AND created_by = auth.uid()
    )
  );

011_create_process_access.sql — tabla de acceso explícito a procesos:

CREATE TABLE IF NOT EXISTS public.process_access (
  process_id    uuid  NOT NULL REFERENCES public.processes(id) ON DELETE CASCADE,
  grantee_type  text  NOT NULL CHECK (grantee_type IN ('user','group')),
  grantee_id    uuid  NOT NULL,
  permission    text  NOT NULL DEFAULT 'view' CHECK (permission IN ('view','edit')),
  PRIMARY KEY (process_id, grantee_type, grantee_id)
);

ALTER TABLE public.process_access ENABLE ROW LEVEL SECURITY;

CREATE POLICY "read_process_access" ON public.process_access
  FOR SELECT USING (auth.uid() IS NOT NULL);

CREATE POLICY "manage_process_access" ON public.process_access
  FOR ALL USING (
    EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND platform_role = 'platform_admin')
    OR EXISTS (
      SELECT 1 FROM public.processes
      WHERE id = process_access.process_id AND owner_id = auth.uid()
    )
  );

012_update_processes_rls.sql — reemplazar política amplia con políticas granulares:

-- Helper: retorna true si el usuario actual es platform_admin
CREATE OR REPLACE FUNCTION public.is_platform_admin()
RETURNS boolean
LANGUAGE sql SECURITY DEFINER
AS $$
  SELECT EXISTS (
    SELECT 1 FROM public.users
    WHERE id = auth.uid() AND platform_role = 'platform_admin'
  );
$$;

-- Eliminar política amplia de Sprint 4 Etapa 2
DROP POLICY IF EXISTS "authenticated_all_processes" ON public.processes;

-- Políticas granulares
CREATE POLICY "select_processes" ON public.processes
  FOR SELECT USING (
    public.is_platform_admin()
    OR owner_id = auth.uid()
    OR EXISTS (
      SELECT 1 FROM public.process_access
      WHERE process_id = processes.id
        AND (
          (grantee_type = 'user'  AND grantee_id = auth.uid())
          OR (grantee_type = 'group' AND grantee_id IN (
            SELECT group_id FROM public.group_memberships WHERE user_id = auth.uid()
          ))
        )
    )
  );

CREATE POLICY "insert_processes" ON public.processes
  FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);

CREATE POLICY "update_processes" ON public.processes
  FOR UPDATE USING (
    public.is_platform_admin()
    OR owner_id = auth.uid()
    OR EXISTS (
      SELECT 1 FROM public.process_access
      WHERE process_id = processes.id
        AND permission = 'edit'
        AND (
          (grantee_type = 'user'  AND grantee_id = auth.uid())
          OR (grantee_type = 'group' AND grantee_id IN (
            SELECT group_id FROM public.group_memberships WHERE user_id = auth.uid()
          ))
        )
    )
  );

CREATE POLICY "delete_processes" ON public.processes
  FOR DELETE USING (public.is_platform_admin() OR owner_id = auth.uid());

2. Tipo Process — agregar ownerId

En src/domain/types.ts, agregar el campo al final de la interfaz Process:

export interface Process {
  // ... campos existentes sin cambios ...
  ownerId: string   // 🆕 Sprint 4 — owner del proceso (auth.uid() al crear)
}

3. Repositorio de procesos — mapear ownerId

En src/persistence/supabase/process-repo.ts:

  • fromRow: agregar ownerId: (row.owner_id as string) ?? ''
  • toRow: el campo owner_id ya se escribe correctamente; verificar que no se sobreescriba en updates (en update, owner_id no debe cambiar — usar el valor del proceso existente, no userId del caller)

Agregar método para gestionar visibilidad:

async setShared(processId: string, shared: boolean): Promise<void>
// si shared=true: no hace nada (la visibilidad para platform_admin ya está por RLS)
// si shared=false: no hace nada en Sprint 4 (sin guests aún)
// En Sprint 4 este método existe para no romper la UI — implementación no-op con comentario explicativo
// La implementación real (insertar/borrar en process_access) llega cuando existan guests

4. LibraryPage — UI de visibilidad

Modificar src/features/library/LibraryPage.tsx:

a) Toggle "Todos / Míos"

Agregar estado local filter: 'all' | 'mine'. Aplicar antes del render:

  • 'mine': mostrar solo procesos donde process.ownerId === currentUser.id
  • 'all': sin filtro adicional

UI: dos botones pill en la barra de herramientas existente, junto al buscador. Usar los colores del sistema (naranja InQ #F59845 para el activo, borde neutro para el inactivo).

b) Badge de visibilidad en tarjetas de proceso

En la tarjeta de cada proceso, agregar un badge pequeño:

  • Proceso propio (ownerId === currentUser.id) → mostrar ícono 🔒 con tooltip "Solo vos"
  • Proceso de otro usuario → mostrar ícono 👤 con tooltip "De otro consultor"

En Sprint 4 todos son platform_admin y ven todo, pero el badge refleja el ownership.

c) Versión con wow — menú de contexto: compartir proceso

En el menú MoreVertical de cada tarjeta propia, agregar opción "Compartir con equipo" (si ownerId === currentUser.id). Al hacer click, llama a setShared(processId, true) del repo y muestra un toast "Proceso compartido con el equipo". En Sprint 4 es una acción no-op en la DB (el método existe pero no escribe aún), pero la UI y el feedback visual funcionan completos. Agregar ícono Share2 de Lucide.


Qué NO entra en esta etapa

  • Implementación real de setShared con escritura en process_access → cuando existan guests
  • "Actualizado por [nombre]" en tarjetas → Etapa 6
  • Header con avatar de usuario → Etapa 6
  • UX de catálogo de recursos → Etapa 7
  • Tabla user_groups en la UI (gestión de grupos) → Sprint futuro
  • Ningún cambio en PDF, simulación o workspace

Orden de ejecución

  1. Crear los tres archivos SQL en supabase/migrations/
  2. Modificar src/domain/types.ts — agregar ownerId
  3. Modificar src/persistence/supabase/process-repo.ts — mapear ownerId + agregar setShared
  4. Modificar src/features/library/LibraryPage.tsx — toggle + badges + menú compartir
  5. npm run build — debe pasar sin errores TypeScript
  6. npm run test — debe mantener 521+ tests verdes

Criterios de validación

  • Archivos SQL 010, 011, 012 creados correctamente en supabase/migrations/
  • Process.ownerId existe en types.ts
  • fromRow en process-repo mapea owner_idownerId
  • setShared existe en process-repo (puede ser no-op con comentario)
  • LibraryPage tiene toggle "Todos / Míos" funcional
  • LibraryPage muestra badge de ownership en cada tarjeta
  • Build limpio sin errores TypeScript
  • 521+ tests verdes
  • Validación visual del director antes del OK formal

Nota sobre los scripts SQL

Los tres scripts (010, 011, 012) se crean en esta etapa pero el director los ejecuta manualmente en Supabase Studio SQL Editor antes del QA visual, en orden numérico. Claude Code no tiene acceso al dashboard de Supabase.