CreaRack-SL

Zoho Fase A · OAuth Service Account + Widget Agenda + Tasks (lectura)

Zoho Fase A · OAuth Service Account + Widget Agenda + Tasks (lectura)

Commit: e17e3ba · 2026-05-16 · 14 archivos · ~900 LOC
ADR de referencia: [[decision—20260516—integracion-zoho-workspace]]

Estado actual (18-05-2026 · s69): El componente ZohoAgendaWidget.tsx (renombrado en s68 a ZohoCalendarWidget.tsx) ha sido eliminado del dashboard home y el archivo borrado del repo. El endpoint /api/zoho/calendar y el resto de la integración (Service Account, OAuth helper, settings panel) permanecen intactos.

Implementación completa de la Fase A de la integración Zoho Workspace en CreaRack Pro. Introduce el modelo Service Account (una autorización de Edu = acceso para todo el equipo), cuatro endpoints API, dos componentes React y una página de settings.


Arquitectura general

[Edu · CF Access] ──GET──▶ /api/oauth/zoho/start
                              │ genera state (D1 zoho_oauth_state)
                              │ redirige a Zoho consent
                              ▼
                    [Zoho consent screen]
                              │ redirect con code + state + accounts-server
                              ▼
                    /api/oauth/zoho/callback  (PUBLIC_PATH)
                              │ valida state (single-use), detecta DC
                              │ exchangeCodeForTokens → upsertToken (D1)
                              ▼
                    [zoho_oauth_tokens id=1]
                              │
              ┌───────────────┼────────────────┐
              ▼               ▼                ▼
    /api/zoho/status  /api/zoho/calendar  /api/zoho/tasks
              │               │                │
              └───────────────▼────────────────┘
                    ZohoAgendaWidget.tsx
                    (bento home · DesktopA)

Endpoints API

GET /api/oauth/zoho/start

Archivo: functions/api/oauth/zoho/start.ts
Auth: CF Access (solo Edu en producción)

  1. Valida que ZOHO_CLIENT_ID y ZOHO_CLIENT_SECRET estén configurados.
  2. Lee el email del autorizador de Cf-Access-Authenticated-User-Email.
  3. Genera un UUID como state y lo persiste en zoho_oauth_state.
  4. Redirige 302 al consent screen de Zoho (dc=eu por defecto, override vía ?dc=).

GET /api/oauth/zoho/callback

Archivo: functions/api/oauth/zoho/callback.ts
Auth: Ninguna (PUBLIC_PATH — la seguridad reside en la validación del state)

Flujo de validación:

  1. Si ?error= presente → HTML de error 400.
  2. Si falta code o state → HTML error 400.
  3. Busca state en zoho_oauth_state (consumo single-use: DELETE inmediato).
  4. Limpieza de estados caducados (> 1 hora).
  5. Detecta DC desde ?accounts-server=accounts.zoho.eu (regex).
  6. Llama a exchangeCodeForTokens → upsertToken.
  7. Devuelve HTML de confirmación con opción de volver al dashboard.

Seguridad: el callback es público porque Zoho redirige sin JWT de CF Access. La protección es la validación del state single-use almacenado en D1 (anti-CSRF).


GET /api/zoho/status · DELETE /api/zoho/status

Archivo: functions/api/zoho/status.ts
Auth: CF Access estándar

  • GET: devuelve {authorized, granted_by, granted_at, dc, updated_at}. Si no hay token: {authorized: false}.
  • DELETE: borra el token singleton (revocación).

GET /api/zoho/calendar?from=ISO&to=ISO&assignee=Edu|Dani|Txell

Archivo: functions/api/zoho/calendar.ts

  1. Obtiene access_token válido (refresco automático vía getValidAccessToken).
  2. Llama a GET /api/v1/calendars del Service Account.
  3. Para cada calendario, agrega eventos en el rango [from, to] (default: próximos 7 días).
  4. Filtro assignee aplicado client-side contra organizer y attendees[].email.
  5. Responde {events[], count, calendars, dc}.

GET /api/zoho/tasks?status=pending|in-progress|done&assignee=&limit=N

Archivo: functions/api/zoho/tasks.ts

  1. Obtiene token válido.
  2. Obtiene grupos del Service Account (/api/tasks/groups).
  3. Agrega tasks personales (/api/tasks/me) + tasks de cada grupo con paginación (máx 5 páginas × 100 items).
  4. Filtros client-side: status (normaliza variantes Zoho → pending/in-progress/done) y assignee (match por nombre).
  5. Ordena por dueDate ASC. Límite máximo: 500.
  6. Responde {tasks[], count, groups, dc}.

Componentes Frontend

ZohoAgendaWidget.tsx

Path: src/components/widgets/ZohoAgendaWidget.tsx

Widget del bento home con dos secciones:

  • Agenda · próximos 7 días → consume /api/zoho/calendar.
  • Tareas Zoho → consume /api/zoho/tasks.

Filtro dropdown: Todos / Edu / Dani / Txell — aplica a ambas secciones vía ?assignee=.

Estado vacío: si /api/zoho/status devuelve authorized: false, muestra mensaje de “no conectado” con enlace a settings.

Integración en DesktopA: añadido como slot bento en la columna derecha, bajo HealthWidget. KanbanMini (D1 tasks) coexiste en su slot original (Ruta Y de coexistencia).


ZohoSettingsPanel.tsx

Path: src/components/settings/ZohoSettingsPanel.tsx

Panel React para /settings/integrations/zoho.astro:

  • No autorizado: botón “Conectar Zoho” → link a /api/oauth/zoho/start + lista de scopes.
  • Autorizado: metadata (granted_by, granted_at, dc, updated_at) + acciones “Re-autorizar” y “Revocar”.
  • En localhost: modo mock (sin llamadas API, authorized: false para desarrollo).

Migración D1 — 0026_create_zoho_oauth.sql

-- Singleton: una sola fila con constraint CHECK (id = 1)
CREATE TABLE IF NOT EXISTS zoho_oauth_tokens (
  id INTEGER PRIMARY KEY CHECK (id = 1),
  refresh_token TEXT NOT NULL,
  access_token TEXT,
  access_token_expires_at TEXT,
  dc TEXT NOT NULL DEFAULT 'eu',
  granted_by TEXT NOT NULL,
  granted_at TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Anti-CSRF: TTL 10 min, limpiados por callback
CREATE TABLE IF NOT EXISTS zoho_oauth_state (
  state TEXT PRIMARY KEY,
  created_by TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_zoho_oauth_state_created ON zoho_oauth_state(created_at);

Cambios en middleware (functions/api/_middleware.ts)

/api/oauth/zoho/callback añadido a PUBLIC_PATHS — el callback necesita ser accesible sin JWT de CF Access porque Zoho redirige directamente desde su dominio.


Decisiones de diseño notables

DecisiónMotivo
Singleton id=1 con CHECK constraintGarantiza que nunca haya más de un Service Account por workspace
State single-use consumido en callbackAnti-CSRF: un state válido solo se puede usar una vez
Filtro assignee client-sideLa Zoho Calendar API no soporta filtrado server-side por attendee en v1
Filtro status client-side en TasksLa Zoho Tasks API tampoco permite filtrado por status server-side
localhost mock en ZohoSettingsPanelEvita llamadas reales durante desarrollo local sin token configurado
Ruta Y (coexistencia)KanbanMini tiene drag-and-drop; sin PUT Zoho no es posible sustituirlo en Fase A

Estado y fases futuras

  • Fase A ✅ — Este PR. Lectura Calendar + Tasks. OAuth Service Account.
  • Fase B ⏳ — Escritura de tasks desde CreaRack (POST Zoho).
  • Fase C ⏳ — Notificaciones vía Zoho.
  • Fase D ⏳ — Drag-and-drop en widget Zoho (PUT) + migración masiva D1→Zoho.

Véase también

  • [[decision—20260516—integracion-zoho-workspace]]
  • [[entity—zoho—service—zoho-oauth-helper]]