Files
inq-roi-simulador-web/sprints/sprint-4/BRIEF.md
Marcos Benítez 78e776df6b feat(sprint-4/etapa-1): Supabase auth + Google OAuth + LoginPage + rutas protegidas
- Cliente Supabase singleton en src/lib/supabase.ts
- Interfaces AuthUser / AuthService en src/auth/types.ts
- SupabaseAuthService: signInWithGoogle, signOut, getCurrentUser, onAuthStateChange
  con upsert a tabla users (platform_admin para @inquality.com.py, guest resto)
- AuthProvider (React Context) wrappea toda la app; expone user, loading, signIn, signOut
- LoginPage /login: logo InQuality color, título, fade-in, botón Google naranja #F59845
- RootComponent del router guarda todas las rutas — sin sesión → /login, con sesión + /login → /
- Script SQL: supabase/migrations/001_create_users.sql (tabla users + RLS)
- AppHeader: <Link> → <a href="/"> para evitar dependencia de RouterContext en tests
- 561/561 tests verdes, build sin errores TS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:53:12 -03:00

19 KiB

Sprint 4 — BRIEF

Naturaleza del sprint: refactorización arquitectural mayor. No hay features de producto nuevas visibles. El objetivo es habilitar colaboración multi-usuario sobre una base correcta. Este sprint es una excepción justificada al rebalanceo web/PDF/infra — es 100% infraestructura.


Contexto y motivación

InQ ROI nació como herramienta single-user con persistencia local (IndexedDB/Dexie). El equipo de InQuality lo usa actualmente de forma individual y quiere pasar a un modelo colaborativo donde:

  • Múltiples consultores pueden trabajar sobre procesos compartidos
  • Existe identidad de usuario (quién creó, quién actualizó)
  • Algunos elementos son privados del consultor, otros compartidos con el equipo
  • Los recursos son un catálogo global del equipo, no duplicados por proceso

Para esto se requiere reemplazar IndexedDB con una base de datos centralizada, agregar autenticación, y rediseñar el modelo de datos de recursos.

Dato importante: no hay datos en producción que valgan la pena migrar. Se abandona IndexedDB limpiamente y se empieza de cero en la nueva base de datos.


Decisiones estratégicas — NO reabrir en esta sprint

Auth: Supabase ahora, Keycloak después

Se usa Supabase como BaaS para Sprint 4: auth + PostgreSQL + RLS. La razón es velocidad de implementación.

El código debe construir una interfaz AuthService que abstraiga el proveedor de autenticación. El objetivo es que la migración futura a Keycloak (servidor de identidad InQuality para múltiples productos) sea cambio de implementación, no reescritura de la app.

InQuality tiene infraestructura VPS (desa + prod) y la visión es Keycloak self-hosted como plataforma de identidad para todos sus productos. Eso es trabajo de un sprint futuro.

Modelo de plataforma: InQuality como operador (Modelo A)

InQ ROI es una plataforma de consultoría operada por InQuality, no un SaaS peer-to-peer. InQuality controla quién entra, a qué tiene acceso, y qué puede hacer. Los clientes y consultores externos son guests invitados por InQuality.

Esto se implementa con un campo platform_role en la tabla de usuarios:

  • platform_admin → staff InQuality, puede ver todos los procesos de todos los usuarios/grupos
  • member → usuario estándar (consultor InQuality sin acceso global, o reservado para futuro)
  • guest → cliente externo o consultor externo, solo ve lo que se le comparte explícitamente

En Sprint 4, todos los usuarios de InQuality reciben platform_admin. El rol guest se usa cuando se agregue el primer cliente externo.

Migración a Modelo B (SaaS peer-to-peer) — posible, no planificada. Cuando InQuality decida que cada organización cliente debe ser autónoma (administrar sus propios usuarios, sin acceso implícito de InQuality), la migración consiste en: eliminar el bypass de platform_admin, agregar self-service de org, y cambiar las políticas RLS. El schema de Sprint 4 ya lo soporta sin reestructurar datos.

Auth: providers múltiples según rol

  • Staff InQuality (platform_admin): Google Workspace @inquality.com.py. Solo este dominio puede obtener platform_admin.
  • Guests (clientes, consultores externos): cualquier email. Magic link o email+contraseña. No requieren Google Workspace.

Supabase soporta múltiples providers simultáneamente. En Sprint 4 solo se implementa Google OAuth para staff. La infraestructura para guest auth (magic link) se deja preparada pero no activada hasta que haya un cliente real.

Online-only

Se elimina el requerimiento de offline. La herramienta requiere conectividad. Offline + sync se agrega al roadmap futuro.

Sin auditoría en Sprint 4

La auditoría de cambios (quién cambió qué campo y cuándo) se difiere a un sprint dedicado. En Sprint 4 solo se captura owner_id, created_by, updated_by, created_at, updated_at en las tablas principales — suficiente para mostrar identidad en la UI sin el costo de instrumentar un audit log completo.


Modelo de datos objetivo

Entidades centrales y sus relaciones

El modelo tiene tres entidades de identidad/acceso y las entidades de negocio del producto. La separación es importante: client en un proceso es un atributo de texto, no una entidad con acceso al sistema.

Usuarios (identidad)
  ↓ pertenecen a
Grupos de usuario (agrupación de acceso)
  ↓ se usan en
Acceso a procesos (tabla de permisos)
  ↓ determina quién ve
Procesos → Actividades → Simulaciones (entidades de negocio)
           ↑
Recursos (catálogo global, sin process_id)

Usuarios

La tabla de usuarios combina Supabase Auth (fuente de verdad para credenciales) con una tabla users en la DB de la app para atributos de la plataforma.

users
  id               uuid PK (mismo UUID que auth.users de Supabase)
  email            text NOT NULL
  name             text
  avatar_url       text
  platform_role    'platform_admin' | 'member' | 'guest'
  created_at       timestamptz DEFAULT now()

Reglas de platform_role:

  • platform_admin → staff InQuality. Bypass de todos los controles de acceso: ve y puede editar todos los procesos. Solo usuarios con dominio @inquality.com.py obtienen este rol.
  • member → usuario estándar. Ve solo lo que se le comparte explícitamente. Reservado para consultores InQuality sin acceso global o para uso futuro.
  • guest → cliente externo o consultor externo. Idéntico a member en permisos. Se usa para distinguir visualmente en la UI quién es externo.

En Sprint 4, todos los usuarios de InQuality reciben platform_admin. El rol guest entra cuando llegue el primer cliente externo.

El campo platform_role nunca hardcodea "inquality". Si en el futuro InQuality decide operar el Modelo B (cada org autónoma), el bypass se elimina cambiando las políticas RLS — el campo permanece genérico.

Grupos de usuario

Permiten compartir un proceso con un conjunto de personas sin listar a cada una individualmente. Útil para "todos los del cliente Acme" o "equipo de consultoría junior".

user_groups
  id               uuid PK
  name             text NOT NULL
  created_by       uuid → users.id
  created_at       timestamptz DEFAULT now()

group_memberships
  user_id          uuid → users.id
  group_id         uuid → user_groups.id
  role             'owner' | 'member'
  PRIMARY KEY (user_id, group_id)

Acceso a procesos

Reemplaza el campo visibility: 'private' | 'team' del modelo anterior. El acceso es explícito: el owner de un proceso elige con quién compartirlo (usuarios individuales o grupos).

process_access
  process_id       uuid → processes.id
  grantee_type     'user' | 'group'
  grantee_id       uuid  (→ users.id si 'user', → user_groups.id si 'group')
  permission       'view' | 'edit'
  PRIMARY KEY (process_id, grantee_type, grantee_id)

Un proceso sin entradas en process_access es privado del owner. Un proceso compartido con el grupo "Equipo InQuality" es efectivamente de visibilidad de equipo. La tabla process_access es suficientemente flexible para Modelo A y Modelo B sin reestructurar.

Procesos

processes
  id          uuid PK
  name        text
  client      text          ← atributo descriptivo, NO entidad con acceso
  currency    text
  frequency   numeric
  bpmn_xml    text
  owner_id    uuid → users.id
  created_by  uuid → users.id
  updated_by  uuid → users.id
  created_at  timestamptz
  updated_at  timestamptz

Sin campo visibility. El acceso se determina completamente por process_access y por platform_role del usuario.

Recursos — catálogo global

Este es el cambio más significativo de modelo. Los recursos dejan de ser por proceso y pasan a ser un catálogo global del equipo.

resources
  id             uuid PK
  name           text
  type           'human' | 'system' | 'tool'
  cost_per_hour  numeric
  created_by     uuid → users.id
  updated_by     uuid → users.id
  created_at     timestamptz
  updated_at     timestamptz

Sin process_id. Sin control de acceso propio — los recursos son siempre visibles para todos los usuarios autenticados. Cualquier consultor puede crear, ver y editar recursos del catálogo.

Las asignaciones de recursos a actividades (activity_resource_assignments) referencian el resource_id del catálogo global.

Actividades

Sin cambio de modelo respecto a Sprint 3. Se agrega created_by, updated_by, created_at, updated_at.

Simulaciones

No tienen control de acceso propio — la regla de acceso se deriva del proceso al que pertenecen.

Grupos de procesos y tags

Compartidos por todo el equipo. Sin campo de acceso. Sin owner_id.


Interfaz AuthService — contrato obligatorio

Claude Code debe definir y respetar esta interfaz. No llamar a Supabase directamente desde componentes o stores.

interface AuthUser {
  id: string
  email: string
  name: string
  avatarUrl?: string
}

interface AuthService {
  signInWithGoogle(): Promise<void>
  signOut(): Promise<void>
  getCurrentUser(): Promise<AuthUser | null>
  onAuthStateChange(callback: (user: AuthUser | null) => void): () => void
}

La implementación SupabaseAuthService implementa esta interfaz y es la única que toca Supabase directamente. Cuando llegue Keycloak, se escribe KeycloakAuthService que implementa la misma interfaz. El resto de la app no cambia.


Capa de repositorios — contrato obligatorio

Actualmente existe src/persistence/repositories.ts con Dexie. Se reemplaza por implementaciones Supabase manteniendo el mismo patrón de repositorios.

// El contrato (interface) no cambia, la implementación sí
interface ProcessRepository {
  getAll(): Promise<Process[]>
  getById(id: string): Promise<Process | undefined>
  save(process: Process): Promise<void>
  delete(id: string): Promise<void>
  // ...
}

Los stores de Zustand llaman al repositorio. Los repositorios llaman a Supabase. Ningún componente toca Supabase directamente.


Row-Level Security (RLS)

Las políticas de RLS de Supabase deben ser equivalentes a políticas PostgreSQL estándar. No usar features exclusivas de Supabase que no puedan recrearse en un PostgreSQL standalone cuando se migre a VPS.

Helper function (crear una vez)

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

Políticas para processes

-- Ver: platform_admin ve todo | owner ve los suyos | acceso explícito
CREATE POLICY "select_processes" ON processes FOR SELECT
  USING (
    is_platform_admin()
    OR owner_id = auth.uid()
    OR EXISTS (
      SELECT 1 FROM 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 group_memberships WHERE user_id = auth.uid()
          ))
        )
    )
  );

-- Insertar: cualquier usuario autenticado
CREATE POLICY "insert_processes" ON processes FOR INSERT
  WITH CHECK (auth.uid() IS NOT NULL);

-- Actualizar: platform_admin | owner | quien tenga permiso 'edit'
CREATE POLICY "update_processes" ON processes FOR UPDATE
  USING (
    is_platform_admin()
    OR owner_id = auth.uid()
    OR EXISTS (
      SELECT 1 FROM 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 group_memberships WHERE user_id = auth.uid()
          ))
        )
    )
  );

-- Eliminar: solo el owner (platform_admin puede, por ser owner o por override)
CREATE POLICY "delete_processes" ON processes FOR DELETE
  USING (is_platform_admin() OR owner_id = auth.uid());

Políticas para resources (catálogo global)

-- Todos los autenticados pueden leer y escribir
CREATE POLICY "all_resources" ON resources
  USING (auth.uid() IS NOT NULL)
  WITH CHECK (auth.uid() IS NOT NULL);

Políticas para users, user_groups, group_memberships, process_access

-- users: cualquier autenticado puede leer (para mostrar nombres), solo el propio usuario puede actualizar
CREATE POLICY "read_users" ON users FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "update_own_user" ON users FOR UPDATE USING (id = auth.uid());

-- user_groups: todos los autenticados leen; platform_admin o owner del grupo puede modificar
CREATE POLICY "read_groups" ON user_groups FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "manage_groups" ON user_groups FOR ALL
  USING (is_platform_admin() OR created_by = auth.uid());

-- group_memberships: todos los autenticados leen; platform_admin o owner del grupo gestiona
CREATE POLICY "read_memberships" ON group_memberships FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "manage_memberships" ON group_memberships FOR ALL
  USING (
    is_platform_admin()
    OR EXISTS (
      SELECT 1 FROM user_groups
      WHERE id = group_memberships.group_id AND created_by = auth.uid()
    )
  );

-- process_access: owner del proceso o platform_admin gestiona los accesos
CREATE POLICY "manage_process_access" ON process_access FOR ALL
  USING (
    is_platform_admin()
    OR EXISTS (
      SELECT 1 FROM processes WHERE id = process_access.process_id AND owner_id = auth.uid()
    )
  );
CREATE POLICY "read_process_access" ON process_access FOR SELECT
  USING (auth.uid() IS NOT NULL);

Nota sobre migración a Modelo B

Para migrar de Modelo A a Modelo B (orgs autónomas), los únicos cambios son:

  1. Eliminar o condicionar el bypass de is_platform_admin() en las políticas
  2. Agregar tabla organizations y org_memberships
  3. Ajustar las políticas para que el scope sea org_id en lugar de plataforma global

El schema de datos no cambia. Las tablas user_groups, group_memberships y process_access son compatibles con ambos modelos.


UX — cambios de superficie

Login

Pantalla de login antes de cualquier contenido. Dos flujos:

Staff InQuality → Botón "Iniciar sesión con Google". Solo cuentas @inquality.com.py obtienen acceso. Al autenticarse, la app verifica el dominio y asigna platform_role = 'platform_admin' si no existe registro previo en la tabla users.

Guests (clientes, consultores externos) → En Sprint 4 este flujo no está activado. La infraestructura (campo platform_role = 'guest', tabla users) se construye lista para habilitarlo. El botón de magic link / email no aparece en la UI de Sprint 4. Se activa cuando haya un cliente real que incorporar.

Al completar login, la app redirige al workspace. Sin login, cualquier ruta protegida redirige a /login.

Biblioteca (LibraryPage)

  • Toggle de filtro: "Todos" / "Míos"
  • Badge o ícono que indica visibilidad (privado 🔒 / compartido)
  • En cada tarjeta de proceso: "Actualizado por [nombre]" + timestamp relativo
  • Botón para cambiar visibilidad de un proceso propio

ResourcesPanel (en workspace)

El panel de recursos ya no es "crear recursos para este proceso". Pasa a ser "seleccionar desde catálogo global" + "agregar nuevo al catálogo". El catálogo vive en una nueva sección de la app (Settings o panel dedicado).

AppHeader

Mostrar avatar / nombre del usuario logueado. Botón de logout.


Stack — cambios

Agregado Propósito
@supabase/supabase-js Cliente Supabase (auth + db)
Variables de entorno VITE_SUPABASE_URL y VITE_SUPABASE_ANON_KEY Configuración por entorno
Eliminado Motivo
dexie Reemplazado por Supabase PostgreSQL
src/persistence/db.ts Reemplazado por cliente Supabase
Migraciones Dexie (v1-v4) Ya no aplican

Las variables de entorno deben existir en .env.local (ignorado por git) y documentarse en .env.example.


Etapas tentativas

El orden importa — cada etapa habilita la siguiente.

Etapa Descripción
1 Supabase setup + AuthService + login page + Google OAuth
2 Schema PostgreSQL + migración de repositorios (procesos + grupos + tags)
3 Migración de actividades + recursos globales (nuevo modelo)
4 Migración de simulaciones + limpieza de Dexie
5 Modelo de privacidad: RLS + UI de visibilidad en biblioteca
6 Identidad en UI: header con usuario + "actualizado por" en biblioteca y workspace
7 ResourcesPanel: UX de catálogo global
8 Tests de integración + polish + validación E2E de auth

Cada etapa tiene su propio ETAPA_N_PROMPT.md antes de arrancar. No improvisar orden ni scope entre etapas.


Criterios de aceptación del sprint

  • Login con Google Workspace (@inquality.com.py) funciona en desa y prod
  • Ruta protegida — sin login no se accede a nada de la app
  • Procesos se guardan y recuperan de Supabase (no de IndexedDB)
  • Control de acceso funciona — un proceso sin process_access no lo ve otro usuario (salvo platform_admin)
  • Recursos son globales — visibles para todos los usuarios autenticados
  • En la biblioteca aparece "Actualizado por [nombre]" en cada proceso
  • AuthService es una interface con implementación SupabaseAuthService
  • No hay llamadas directas a Supabase fuera de repositorios y AuthService
  • Dexie eliminado del proyecto (no solo sin uso — removido del package.json)
  • Build limpio sin errores TypeScript
  • Tests de dominio previos siguen verdes (simulación, ROI, PDF)
  • Validación visual del director en cada etapa con output visual

Riesgos y mitigaciones

Google Workspace OAuth requiere configuración en Google Cloud Console. Acción: crear el proyecto OAuth antes de que Claude Code escriba una línea de código. El client_id y client_secret tienen que estar disponibles. Se configura también en Supabase Dashboard → Auth → Providers.

El cambio de recursos globales rompe el UX actual completamente. Acción: el prompt de Etapa 7 (ResourcesPanel) debe incluir un mockup del nuevo UX aprobado antes de implementar. Aplicar Patrón C.5 (mockup como contrato visual).

Múltiples etapas tienen riesgo de regresión en simulación/PDF. Acción: correr npm run test al final de cada etapa y verificar que los 561 tests previos siguen verdes. Si baja el count, parar y reportar antes de continuar.


Fuera del scope de Sprint 4 — explícito

  • Auditoría de cambios (audit log) — sprint dedicado futuro
  • Keycloak / migración de auth provider — sprint futuro
  • Offline mode / sync — roadmap
  • Acceso de clientes externos — Fase 2
  • Versionado de procesos — Fase 2
  • Export a Excel — BACKLOG sin sprint
  • Cualquier feature de PDF o simulación nueva

Referencias

Recurso Propósito
simulador-web/CLAUDE.md Reglas de implementación para Claude Code
simulador-web/src/persistence/ Código Dexie actual a reemplazar
simulador-web/src/store/ Stores Zustand que llaman a repositorios
simulador-web/src/domain/types.ts Tipos de dominio (base del schema PostgreSQL)
methodology/PATTERNS.md C.5 (mockup como contrato), C.7 (git diff preventivo)
methodology/ANTI_PATTERNS.md AP.8 (adelanto silencioso)
Supabase docs — RLS https://supabase.com/docs/guides/auth/row-level-security
Supabase docs — Google OAuth https://supabase.com/docs/guides/auth/social-login/auth-google