Integración Zoho Calendar + Mail con OAuth per-user (s68)
Integración Zoho Calendar + Mail con OAuth per-user (s68)
PR: #45
Merge: 2026-05-17
ADR: [[decision—20260517—integracion-zoho-calendar-mail-pivot]]
Estado actual (18-05-2026 · s69): El widget renombrado
ZohoCalendarWidgetha sido eliminado del dashboard home y su archivo borrado del repo. El endpoint/api/zoho/calendary la integración Calendar + Mail en task hub (Modal, TaskCalendar, badges) permanecen intactos.
Resumen
El s68 pivota la integración Zoho de un modelo Service Account (singleton) a un modelo OAuth per-user donde cada miembro del staff (Edu / Dani / Txell) autoriza su propia cuenta. Al mismo tiempo, descarta Zoho Tasks como destino de sincronización y convierte la task de D1 en hub con Zoho Calendar y Zoho Mail como brazos ejecutores.
Arquitectura resultante
TaskModal
├── [Crear evento] → POST /api/zoho/calendar
│ → POST /api/tasks/{id}/links/calendar
├── [Vincular email] → GET /api/zoho/mail/search
│ → POST /api/tasks/{id}/links/email
└── [Nuevo email] → GET /api/zoho/mail/compose-url (deep-link)
KanbanMini / TaskKanban / TaskTableView / TaskCalendar
└── badges (calendar_event_count, email_count) — subqueries en SELECT tasks.*
El actor (quién hace la llamada) se resuelve en cada endpoint via functions/_lib/staff.ts leyendo el header Cf-Access-Authenticated-User-Email.
Componentes nuevos o modificados
Backend (CF Pages Functions)
| Archivo | Tipo | Descripción |
|---|---|---|
functions/_lib/staff.ts | Nuevo | Mapeo CF Access email → TeamMember. SoT compartido. |
functions/_lib/zoho.ts | Modificado | Per-user model: todas las funciones aceptan teamMember. |
functions/api/oauth/zoho/start.ts | Modificado | Valida que el actor sea TeamMember antes de iniciar OAuth. |
functions/api/oauth/zoho/callback.ts | Modificado | Resuelve teamMember desde el state; guarda zoho_email. |
functions/api/zoho/calendar.ts | Modificado | GET per-user + nuevo POST para crear eventos. |
functions/api/zoho/mail/search.ts | Nuevo | Búsqueda de emails en Zoho Mail del actor. |
functions/api/zoho/mail/compose-url.ts | Nuevo | Genera deep-link a Zoho Mail compose. |
functions/api/tasks/[id]/links/calendar.ts | Nuevo | CRUD vínculos task ↔ evento Zoho Calendar. |
functions/api/tasks/[id]/links/email.ts | Nuevo | CRUD vínculos task ↔ mensaje Zoho Mail. |
functions/api/tasks/index.ts | Modificado | Añade calendar_event_count y email_count a la query base. |
functions/api/me.ts | Modificado | Usa resolveTeamMember de staff.ts en lugar de tabla inline. |
Frontend
| Componente | Cambio |
|---|---|
TaskModal | Nueva sección TaskZohoLinks con 3 acciones (crear evento, vincular email, nuevo email). |
KanbanMini | Badge de count calendario + email (sin iconos). |
TaskKanban / TaskTableView / TaskCalendar | Idem badges. |
ZohoAgendaWidget → ZohoCalendarWidget | Renombrado; eliminada sección “Tareas Zoho”. |
| Settings integrations/zoho | 3 cards per-user con estado de autorización. |
D1 (Migration 0027)
-- zoho_oauth_tokens: singleton → per-user
ALTER TABLE zoho_oauth_tokens DROP COLUMN id;
-- (simplificado: ver migration real para DDL exacto)
-- Nueva PK: team_member TEXT
-- Nueva col: zoho_email TEXT
-- task_calendar_events: nueva
CREATE TABLE task_calendar_events (
task_id INTEGER NOT NULL REFERENCES tasks(id),
zoho_event_uid TEXT NOT NULL,
calendar_uid TEXT NOT NULL,
title TEXT,
start_at TEXT,
end_at TEXT,
url TEXT,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (task_id, zoho_event_uid)
);
-- task_emails: nueva
CREATE TABLE task_emails (
task_id INTEGER NOT NULL REFERENCES tasks(id),
zoho_message_id TEXT NOT NULL,
thread_id TEXT,
subject TEXT,
from_address TEXT,
email_date TEXT,
url TEXT,
linked_by TEXT NOT NULL,
linked_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (task_id, zoho_message_id)
);
Endpoints nuevos/modificados
GET /api/zoho/calendar
Lee eventos del calendar del actor autenticado (per-user). Eliminado el filtro assignee client-side.
Respuesta incluye team_member para debug.
POST /api/zoho/calendar
Nuevo. Crea un evento en el primary calendar del actor.
Body: { title, description?, start (ISO), end (ISO), isallday?, location?, attendees?: string[] }
Respuesta: { uid, calendar_uid, calendar_name, organizer, url, team_member }
GET /api/zoho/mail/search
Nuevo. Busca mensajes en Zoho Mail del actor.
GET /api/zoho/mail/compose-url
Nuevo. Genera deep-link https://mail.zoho.{dc}/zm/#mail/compose?to=...&subject=...&body=....
No llama a Zoho API; solo necesita el dc del token almacenado.
GET|POST|DELETE /api/tasks/{id}/links/calendar
Nuevo. CRUD de vínculos task ↔ evento Zoho Calendar en task_calendar_events.
GET|POST|DELETE /api/tasks/{id}/links/email
Nuevo. CRUD de vínculos task ↔ mensaje Zoho Mail en task_emails.
Scopes Zoho
Añadidos: ZohoCalendar.event.CREATE · ZohoMail.messages.READ
Retirados: ZohoMail.tasks.READ · ZohoMail.tasks.CREATE (y similares)
⚠️ Todos los miembros del staff deben re-autorizar en /settings/integrations/zoho tras el deploy.
ZohoTokenRow (s68)
interface ZohoTokenRow {
team_member: TeamMember; // PK (era id: number)
zoho_email: string; // Nuevo: email Zoho real del cuenta
refresh_token: string;
access_token: string | null;
access_token_expires_at: string | null;
dc: string;
granted_by_cf_email: string; // Renombrado de granted_by
granted_at: string;
updated_at: string;
}
Lo que NO cambia (continuidad)
- D1 es SoT único para tasks. El modelo de tasks, KanbanMini y todas las vistas son intactos.
- Zoho Tasks no se usa. No hay sync bidireccional ni migración masiva.
- El flujo OAuth (estado anti-CSRF en
zoho_oauth_state, callback conaccounts-server) sigue igual; solo cambia que ahora persiste porteam_member.
Véase también
- [[decision—20260517—integracion-zoho-calendar-mail-pivot]]
- [[entity—workers—service—staff]]
- [[entity—workers—service—zoho-lib]]
- [[entity—workers—table—zoho-oauth-tokens]]
- [[entity—workers—table—task-calendar-events]]
- [[entity—workers—table—task-emails]]