CreaRack-SL

Zoho OAuth — Modo admin ?as=X para pre-autorización de onboarding

Zoho OAuth — Modo admin ?as=X para pre-autorización de onboarding

Resumen

El endpoint GET /api/oauth/zoho/start acepta un parámetro opcional ?as=Edu|Dani|Txell que permite a Edu autorizar la cuenta Zoho de otro miembro del staff desde su propia sesión CF Access, sin que ese miembro tenga que pasar por el flujo OAuth.

Motivación

Durante el onboarding nocturno de la sprint s68, Edu disponía de las credenciales Zoho de Dani y Txell pero ellos aún no habían pasado por el flujo OAuth desde su sesión. Las alternativas eran costosas:

  • Sesiones incógnito con sus credenciales.
  • Magic links que requerían que ellos estuvieran disponibles.

El modo ?as=X permite a Edu pre-autorizar sus cuentas de una sola vez desde su propia sesión autenticada.

Flujo técnico

Edu (sesión CF Access activa)
  │
  ├─ GET /api/oauth/zoho/start?as=Dani
  │     resolveActorEmail() → "edu@edomo.net"
  │     resolveTeamMember() → "Edu"
  │     asParam = "Dani" → isTeamMember("Dani") = true
  │     targetMember = "Dani"
  │     state = randomUUID()
  │     stateValue = JSON.stringify({ email: "edu@edomo.net", target: "Dani" })
  │     INSERT zoho_oauth_state (state, stateValue, now)
  │     → redirect a Zoho authorize URL
  │
  ├─ [Edu se autentica en Zoho como Dani]
  │
  └─ GET /api/oauth/zoho/callback?code=...&state=...
        parseStateValue(stateRow.created_by)
          → { email: "edu@edomo.net", target: "Dani" }
        isTeamMember("Dani") = true → teamMember = "Dani"
        exchangeCodeForTokens() → tokens
        upsertToken(env, "Dani", ..., "edu@edomo.net")
        fetchPrimaryZohoEmail() → UPDATE zoho_email

Seguridad

ControlImplementación
Solo Edu puede usar ?as=Xif (actor !== 'Edu') → 403 en start.ts
Valores válidos para ?asisTeamMember(asParam) — solo Edu, Dani, Txell
AuditoríaEl campo created_by guarda { email: "edu@...", target: "Dani" } — quién autorizó y para quién
State single-usezoho_oauth_state se consume en callback y se limpian estados >1h
Callback público pero protegidoValidación del state UUID contra D1 antes de procesar

Compatibilidad hacia atrás

El callback callback.ts incorpora parseStateValue() que maneja dos formatos del campo created_by:

  • Nuevo (post-s68): JSON { email: string, target: string } — usa target directamente como teamMember.
  • Antiguo (pre-s68): email plano como string — target queda vacío, se resuelve teamMember desde email via resolveTeamMemberFromEmail().

Uso

# Edu re-autoriza su propia cuenta
GET /api/oauth/zoho/start

# Edu autoriza en nombre de Dani (onboarding)
GET /api/oauth/zoho/start?as=Dani

# Edu autoriza en nombre de Txell (onboarding)
GET /api/oauth/zoho/start?as=Txell

Nota: Esta feature requiere que Edu tenga sesión CF Access activa y haga login en Zoho con las credenciales del miembro destino durante el flujo OAuth.

Archivos afectados

ArchivoCambio
functions/api/oauth/zoho/start.tsAñade lógica ?as=X, serializa state como JSON
functions/api/oauth/zoho/callback.tsAñade parseStateValue(), resuelve target desde state JSON
functions/_lib/staff.tsNueva resolveActorEmail(), nueva isTeamMember()

Historial

Introducida en commit e65dde4 (fix #46, 2026-05-17). Co-autores: @Esquembri, Claude Opus 4.7.

Véase también

  • [[entity—workers—function—resolve-actor-email]]
  • [[decision—20260517—integracion-zoho-calendar-mail-pivot]]
  • [[feature—zoho—oauth-per-user]]
  • [[entity—workers—endpoint—oauth-zoho-start]]
  • [[entity—workers—endpoint—oauth-zoho-callback]]