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 aZohoCalendarWidget.tsx) ha sido eliminado del dashboard home y el archivo borrado del repo. El endpoint/api/zoho/calendary 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)
- Valida que
ZOHO_CLIENT_IDyZOHO_CLIENT_SECRETestén configurados. - Lee el email del autorizador de
Cf-Access-Authenticated-User-Email. - Genera un UUID como
statey lo persiste enzoho_oauth_state. - Redirige
302al consent screen de Zoho (dc=eupor 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:
- Si
?error=presente → HTML de error 400. - Si falta
codeostate→ HTML error 400. - Busca
stateenzoho_oauth_state(consumo single-use: DELETE inmediato). - Limpieza de estados caducados (> 1 hora).
- Detecta DC desde
?accounts-server=accounts.zoho.eu(regex). - Llama a
exchangeCodeForTokens→upsertToken. - 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
statesingle-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
- Obtiene
access_tokenválido (refresco automático víagetValidAccessToken). - Llama a
GET /api/v1/calendarsdel Service Account. - Para cada calendario, agrega eventos en el rango
[from, to](default: próximos 7 días). - Filtro
assigneeaplicado client-side contraorganizeryattendees[].email. - Responde
{events[], count, calendars, dc}.
GET /api/zoho/tasks?status=pending|in-progress|done&assignee=&limit=N
Archivo: functions/api/zoho/tasks.ts
- Obtiene token válido.
- Obtiene grupos del Service Account (
/api/tasks/groups). - Agrega tasks personales (
/api/tasks/me) + tasks de cada grupo con paginación (máx 5 páginas × 100 items). - Filtros client-side:
status(normaliza variantes Zoho →pending/in-progress/done) yassignee(match por nombre). - Ordena por
dueDateASC. Límite máximo: 500. - 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: falsepara 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ón | Motivo |
|---|---|
Singleton id=1 con CHECK constraint | Garantiza que nunca haya más de un Service Account por workspace |
| State single-use consumido en callback | Anti-CSRF: un state válido solo se puede usar una vez |
| Filtro assignee client-side | La Zoho Calendar API no soporta filtrado server-side por attendee en v1 |
| Filtro status client-side en Tasks | La Zoho Tasks API tampoco permite filtrado por status server-side |
localhost mock en ZohoSettingsPanel | Evita 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]]