CreaRack-SL

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

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

Estado actual (18-05-2026 · s69): El widget ZohoAgendaWidget ha sido eliminado del dashboard home y el archivo src/components/widgets/ZohoCalendarWidget.tsx borrado del repo. El endpoint /api/zoho/calendar se mantiene (lo usan TaskCalendar y TaskModal para vínculos de eventos). Esta página queda como histórico de la implementación Fase A.

Implementación de la Fase A de la integración Zoho en el workspace interno de Esferic Labs (CreaRackSL-workspace). Mergeado en PR#44 el 2026-05-16. ~1.200 LOC añadidas en 14 archivos.


Resumen funcional

Conecta el workspace con Zoho Calendar y Zoho Tasks mediante un modelo de Service Account: un único administrador (Edu) autoriza una sola vez vía OAuth y el refresh_token queda persistido en D1. El resto del staff (Dani, Txell) consume los datos sin pasar por OAuth.

El widget ZohoAgendaWidget aparece en la columna derecha del home (bento bajo “Salud del sistema”) y muestra:

  • Mi agenda · próximos 7 días → eventos de Zoho Calendar.
  • Tareas Zoho → tasks creadas desde Zoho directamente (humanas).
  • Filtro dropdown: Todos / Edu / Dani / Txell (filtra por assignee, attendees, organizer).

Decisión Ruta Y · coexistencia (s67): KanbanMini se mantiene intacto con sus tasks D1 y drag-drop. El widget Zoho se añade como slot adicional en el bento, sin sustituir nada. La migración masiva D1→Zoho queda pospuesta a Fase D.


Archivos introducidos

CapaArchivoLOCDescripción
Migration D1migrations/0026_create_zoho_oauth.sql24Tablas zoho_oauth_tokens (singleton) y zoho_oauth_state (anti-CSRF)
Lib corefunctions/_lib/zoho.ts204OAuth helpers, token refresh automático, zohoFetch
Endpoint OAuthfunctions/api/oauth/zoho/start.ts38Inicia flujo OAuth, genera state anti-CSRF, redirige a Zoho
Endpoint OAuthfunctions/api/oauth/zoho/callback.ts96Callback público, valida state, persiste tokens
Endpoint APIfunctions/api/zoho/calendar.ts89GET /api/zoho/calendar — eventos agregados de todos los calendarios
Endpoint APIfunctions/api/zoho/tasks.ts125GET /api/zoho/tasks — tasks personales + de grupos
Endpoint APIfunctions/api/zoho/status.ts28GET /api/zoho/status + DELETE para revocar
Middlewarefunctions/api/_middleware.ts+6Añade /api/oauth/zoho/callback a PUBLIC_PATHS
Widget Reactsrc/components/widgets/ZohoAgendaWidget.tsx280Widget home con agenda + tareas + filtro
Panel Reactsrc/components/settings/ZohoSettingsPanel.tsx175Panel /settings/integrations/zoho (conectar/re-autorizar/revocar)
Página Astrosrc/pages/settings/integrations/zoho.astro17Shell de la página de ajustes
Homesrc/components/variants/desktop/DesktopA.tsx+20Añade slot bento ZohoAgendaWidget

Modelo de datos D1

zoho_oauth_tokens (singleton — id siempre = 1)

ColumnaTipoDescripción
idINTEGER PRIMARY KEY CHECK (id=1)Singleton; solo existe 1 fila
refresh_tokenTEXT NOT NULLToken permanente, renovable
access_tokenTEXTToken de corta duración (cacheado)
access_token_expires_atTEXTISO 8601; NULL si expirado
dcTEXT DEFAULT 'eu'Data center Zoho (eu, com, in…)
granted_byTEXT NOT NULLEmail CF Access del autorizador
granted_atTEXT NOT NULLTimestamp de la primera autorización
updated_atTEXT NOT NULLÚltima renovación del access_token

zoho_oauth_state (anti-CSRF, efímera)

ColumnaTipoDescripción
stateTEXT PRIMARY KEYUUID generado en /api/oauth/zoho/start
created_byTEXT NOT NULLEmail CF Access del iniciador
created_atTEXT NOT NULLTTL implícito 10 min; limpiado por callback

Flujo OAuth (paso a paso)

Edu  →  GET /api/oauth/zoho/start
         │ genera UUID state → INSERT zoho_oauth_state
         └─ redirect 302 → accounts.zoho.eu/oauth/v2/auth?...

Zoho →  GET /api/oauth/zoho/callback?code=...&state=...&accounts-server=...
         │ valida state (SELECT + DELETE consume single-use)
         │ exchangeCodeForTokens(code, redirectUri, dc)
         └─ upsertToken() → zoho_oauth_tokens id=1

Staff →  GET /api/zoho/calendar | /api/zoho/tasks
          └─ getValidAccessToken(): cache hit o refreshAccessToken() automático

Seguridad del callback público: el endpoint /api/oauth/zoho/callback está en PUBLIC_PATHS del middleware (Zoho redirige sin CF Access JWT). La protección es el state anti-CSRF de uso único. Sin state válido en D1, el callback devuelve 400.


Endpoints expuestos

MétodoRutaAuthDescripción
GET/api/oauth/zoho/startCF Access (solo Edu)Inicia OAuth, genera state, redirige
GET/api/oauth/zoho/callbackPúblico (state validation)Recibe code, persiste tokens, HTML de confirmación
GET/api/zoho/calendarCF Access (todo staff)Eventos Calendar; params: from, to, assignee
GET/api/zoho/tasksCF Access (todo staff)Tasks; params: status, assignee, limit
GET/api/zoho/statusCF Access (todo staff)Estado de autorización (metadata, no tokens)
DELETE/api/zoho/statusCF Access (admin)Revoca tokens (DELETE de la fila singleton)

Lógica de refresh automático

getValidAccessToken() en zoho.ts:

  1. Lee zoho_oauth_tokens WHERE id=1.
  2. Comprueba si access_token_expires_at > now(). Si sí → devuelve el token cacheado.
  3. Si no → llama a refreshAccessToken(), persiste el nuevo access_token + expires_at en D1 (con margen de 60 s), devuelve el fresco.

Esto evita llamadas al endpoint de Zoho en cada request del widget.


Zoho regions

La región se detecta del parámetro accounts-server que Zoho envía en el callback (accounts.zoho.eu → dc=eu). Se almacena en zoho_oauth_tokens.dc y se usa para construir todos los hostnames API posteriores (calendar.zoho.eu, mail.zoho.eu).


Filtros de tasks (client-side)

El endpoint /api/tasks de Zoho no permite filtrar por status server-side. El endpoint del workspace normaliza los valores:

Input status=Zoho status coincidente
pendingnot-started, pending
in-progressin-progress
donecompleted, done

Decisiones de diseño

  • Service Account vs. OAuth individual: un solo consent elimina la fricción para el equipo. Viable porque Calendar+Tasks son compartidos (Esferic Labs SL).
  • Ruta Y (coexistencia): KanbanMini + drag-drop en D1 se preserva intacto. Zoho widget es aditivo. Migración D1→Zoho pospuesta a Fase D.
  • Singleton D1 id=1 CHECK: imposibilita múltiples filas por accidente; la constraint es parte del DDL.
  • Callback público con state single-use: solución estándar para OAuth con redirect externo que no puede pasar por CF Access.

Fases del proyecto Zoho (contexto)

FaseEstadoDescripción
A✅ Completa (PR#44)OAuth Service Account + lectura Calendar + Tasks
BPendienteWrite Tasks desde MCP / workspace
CPendienteReorientar/retirar MCP create_task
DPendienteMigración masiva D1→Zoho + drag-drop en widget Zoho

Véase también

  • [[entity—workspace—service—zoho-lib]]
  • [[decision—20260516—integracion-zoho-workspace]]
  • [[workspace—que-es-workspace]]