9.3 KiB
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 1–4 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 001–009 ejecutados en Supabase Cloud
src/auth/AuthContext.tsxconuseAuth()devolviendo{ user }con al menosuser.idsrc/persistence/supabase/process-repo.tsexistente
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: agregarownerId: (row.owner_id as string) ?? ''toRow: el campoowner_idya se escribe correctamente; verificar que no se sobreescriba en updates (en update,owner_idno debe cambiar — usar el valor del proceso existente, nouserIddel 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 dondeprocess.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
setSharedcon escritura enprocess_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_groupsen la UI (gestión de grupos) → Sprint futuro - Ningún cambio en PDF, simulación o workspace
Orden de ejecución
- Crear los tres archivos SQL en
supabase/migrations/ - Modificar
src/domain/types.ts— agregarownerId - Modificar
src/persistence/supabase/process-repo.ts— mapearownerId+ agregarsetShared - Modificar
src/features/library/LibraryPage.tsx— toggle + badges + menú compartir npm run build— debe pasar sin errores TypeScriptnpm run test— debe mantener 521+ tests verdes
Criterios de validación
- Archivos SQL 010, 011, 012 creados correctamente en
supabase/migrations/ Process.ownerIdexiste entypes.tsfromRowen process-repo mapeaowner_id→ownerIdsetSharedexiste 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.