CreaRack-SL

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 ZohoCalendarWidget ha sido eliminado del dashboard home y su archivo borrado del repo. El endpoint /api/zoho/calendar y 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)

ArchivoTipoDescripción
functions/_lib/staff.tsNuevoMapeo CF Access email → TeamMember. SoT compartido.
functions/_lib/zoho.tsModificadoPer-user model: todas las funciones aceptan teamMember.
functions/api/oauth/zoho/start.tsModificadoValida que el actor sea TeamMember antes de iniciar OAuth.
functions/api/oauth/zoho/callback.tsModificadoResuelve teamMember desde el state; guarda zoho_email.
functions/api/zoho/calendar.tsModificadoGET per-user + nuevo POST para crear eventos.
functions/api/zoho/mail/search.tsNuevoBúsqueda de emails en Zoho Mail del actor.
functions/api/zoho/mail/compose-url.tsNuevoGenera deep-link a Zoho Mail compose.
functions/api/tasks/[id]/links/calendar.tsNuevoCRUD vínculos task ↔ evento Zoho Calendar.
functions/api/tasks/[id]/links/email.tsNuevoCRUD vínculos task ↔ mensaje Zoho Mail.
functions/api/tasks/index.tsModificadoAñade calendar_event_count y email_count a la query base.
functions/api/me.tsModificadoUsa resolveTeamMember de staff.ts en lugar de tabla inline.

Frontend

ComponenteCambio
TaskModalNueva sección TaskZohoLinks con 3 acciones (crear evento, vincular email, nuevo email).
KanbanMiniBadge de count calendario + email (sin iconos).
TaskKanban / TaskTableView / TaskCalendarIdem badges.
ZohoAgendaWidget → ZohoCalendarWidgetRenombrado; eliminada sección “Tareas Zoho”.
Settings integrations/zoho3 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 con accounts-server) sigue igual; solo cambia que ahora persiste por team_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]]