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
ZohoAgendaWidgetha sido eliminado del dashboard home y el archivosrc/components/widgets/ZohoCalendarWidget.tsxborrado del repo. El endpoint/api/zoho/calendarse mantiene (lo usanTaskCalendaryTaskModalpara 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):
KanbanMinise 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
| Capa | Archivo | LOC | Descripción |
|---|---|---|---|
| Migration D1 | migrations/0026_create_zoho_oauth.sql | 24 | Tablas zoho_oauth_tokens (singleton) y zoho_oauth_state (anti-CSRF) |
| Lib core | functions/_lib/zoho.ts | 204 | OAuth helpers, token refresh automático, zohoFetch |
| Endpoint OAuth | functions/api/oauth/zoho/start.ts | 38 | Inicia flujo OAuth, genera state anti-CSRF, redirige a Zoho |
| Endpoint OAuth | functions/api/oauth/zoho/callback.ts | 96 | Callback público, valida state, persiste tokens |
| Endpoint API | functions/api/zoho/calendar.ts | 89 | GET /api/zoho/calendar — eventos agregados de todos los calendarios |
| Endpoint API | functions/api/zoho/tasks.ts | 125 | GET /api/zoho/tasks — tasks personales + de grupos |
| Endpoint API | functions/api/zoho/status.ts | 28 | GET /api/zoho/status + DELETE para revocar |
| Middleware | functions/api/_middleware.ts | +6 | Añade /api/oauth/zoho/callback a PUBLIC_PATHS |
| Widget React | src/components/widgets/ZohoAgendaWidget.tsx | 280 | Widget home con agenda + tareas + filtro |
| Panel React | src/components/settings/ZohoSettingsPanel.tsx | 175 | Panel /settings/integrations/zoho (conectar/re-autorizar/revocar) |
| Página Astro | src/pages/settings/integrations/zoho.astro | 17 | Shell de la página de ajustes |
| Home | src/components/variants/desktop/DesktopA.tsx | +20 | Añade slot bento ZohoAgendaWidget |
Modelo de datos D1
zoho_oauth_tokens (singleton — id siempre = 1)
| Columna | Tipo | Descripción |
|---|---|---|
id | INTEGER PRIMARY KEY CHECK (id=1) | Singleton; solo existe 1 fila |
refresh_token | TEXT NOT NULL | Token permanente, renovable |
access_token | TEXT | Token de corta duración (cacheado) |
access_token_expires_at | TEXT | ISO 8601; NULL si expirado |
dc | TEXT DEFAULT 'eu' | Data center Zoho (eu, com, in…) |
granted_by | TEXT NOT NULL | Email CF Access del autorizador |
granted_at | TEXT NOT NULL | Timestamp de la primera autorización |
updated_at | TEXT NOT NULL | Última renovación del access_token |
zoho_oauth_state (anti-CSRF, efímera)
| Columna | Tipo | Descripción |
|---|---|---|
state | TEXT PRIMARY KEY | UUID generado en /api/oauth/zoho/start |
created_by | TEXT NOT NULL | Email CF Access del iniciador |
created_at | TEXT NOT NULL | TTL 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étodo | Ruta | Auth | Descripción |
|---|---|---|---|
GET | /api/oauth/zoho/start | CF Access (solo Edu) | Inicia OAuth, genera state, redirige |
GET | /api/oauth/zoho/callback | Público (state validation) | Recibe code, persiste tokens, HTML de confirmación |
GET | /api/zoho/calendar | CF Access (todo staff) | Eventos Calendar; params: from, to, assignee |
GET | /api/zoho/tasks | CF Access (todo staff) | Tasks; params: status, assignee, limit |
GET | /api/zoho/status | CF Access (todo staff) | Estado de autorización (metadata, no tokens) |
DELETE | /api/zoho/status | CF Access (admin) | Revoca tokens (DELETE de la fila singleton) |
Lógica de refresh automático
getValidAccessToken() en zoho.ts:
- Lee
zoho_oauth_tokens WHERE id=1. - Comprueba si
access_token_expires_at > now(). Si sí → devuelve el token cacheado. - Si no → llama a
refreshAccessToken(), persiste el nuevoaccess_token+expires_aten 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 |
|---|---|
pending | not-started, pending |
in-progress | in-progress |
done | completed, 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=1CHECK: 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)
| Fase | Estado | Descripción |
|---|---|---|
| A | ✅ Completa (PR#44) | OAuth Service Account + lectura Calendar + Tasks |
| B | Pendiente | Write Tasks desde MCP / workspace |
| C | Pendiente | Reorientar/retirar MCP create_task |
| D | Pendiente | Migració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]]