Skip to main content

Conexión de Instagram (OAuth de Meta)

La conexión de una cuenta de Instagram Business no se crea con POST /connections/create: se hace redirigiendo al usuario a Instagram y volviendo a /connections. El servidor está descrito en Meta; esta página cubre solo lo que hace el frontend.

Flujo paso a paso​

  1. Inicio. El usuario elige "Instagram" en ConnectionChannelModalComponent (alta) o pulsa "Reautorizar" en una tarjeta existente. Ambos llaman a ConnectionsTableComponent.connectInstagram(connection?).
  2. Autorización. connectInstagram pone isAuthorizingInstagram = true (evita dobles clics y deshabilita "Reautorizar") y llama a ConnectionService.authorizeInstagram(connection ? { connectionId } : {}), es decir POST <apiUrl>/meta/oauth/instagram/authorize. El backend responde { data: { url } }.
  3. Salto a Instagram. El frontend hace window.location.href = url (navegación completa, no popup). Si falla la petición, se restablece isAuthorizingInstagram; si el backend responde con error.data.code === 'META_NOT_CONFIGURED' se muestra el toast connections.instagram.errors.notConfigured ("Instagram no está habilitado en este servidor"). ErrorInterceptor deja pasar los errores con data.code sin toast propio, por eso el componente los trata.
  4. Retorno. El backend redirige a /connections?meta=.... ngOnInit llama a handleMetaOAuthReturn(), que lee los query params y los limpia con router.navigate([], { relativeTo, queryParams: {}, replaceUrl: true }) para que recargar no repita el aviso.
  5. Resultado.
    • meta=connected con connectionId: toast de éxito (connections.instagram.connected), findConnectionById(connectionId) y apertura del modal de configuración con justConnected = true.
    • Otro caso: toast de error con la clave connections.instagram.errors.<reason>. Si reason no está en META_OAUTH_REASONS se usa unknown.

Constantes (connections.constants.ts)​

ConstanteValorUso
META_OAUTH_QUERY{ RESULT: 'meta', REASON: 'reason', CONNECTION_ID: 'connectionId' }Nombres de los query params
META_OAUTH_RESULT{ CONNECTED: 'connected', ERROR: 'error' }Valores de meta
META_OAUTH_REASONSdenied, invalid_state, exchange_failed, subscription_failed, account_in_use, account_mismatch, connection_not_foundRazones conocidas
META_NOT_CONFIGURED_CODE'META_NOT_CONFIGURED'Código de error del backend si falta configuración de Meta
INSTAGRAM_EXPIRY_WARNING_DAYS7Umbral del aviso de expiración

Mensajes de error (connections.instagram.errors.*)​

ClaveMensaje (en)
notConfiguredInstagram is not enabled on this server.
deniedYou cancelled the authorization on Instagram.
invalid_stateThe authorization expired or is not valid. Try again.
exchange_failedInstagram did not confirm the authorization. Try again.
subscription_failedMessage delivery could not be enabled for this account. Try again.
account_in_useThis Instagram account is already connected to another company.
account_mismatchYou authorized a different account than this connection's. Log in with the right account.
connection_not_foundThe connection you wanted to reauthorize no longer exists.
unknownInstagram could not be connected. Try again.

El archivo es.json contiene las mismas claves en español.

ConnectionInstagramModalComponent (components/connection-instagram-modal), abierto con NgbModal (backdrop: 'static', centered, size: 'lg').

Entradas: connection (obligatoria) y justConnected (por defecto false).

  • Muestra "Conectado como @cuenta" cuando metaConfig.accountName existe.
  • Con justConnected = true muestra un aviso (connections.instagram.completeSetup: "elige al menos un departamento para que los mensajes se asignen a tu equipo") y marca el formulario como tocado para que se vean los campos obligatorios.
  • Formulario reactivo (mismos campos que el modal de WhatsApp): name (obligatorio, máx. 100), greetingMessage, farewellMessage, departmentIds (obligatorio, selección múltiple con ng-select; los departamentos vienen de DepartmentService.getAll()), reopenToleranceMinutes (obligatorio, mínimo 1, por defecto 15) y returnToLastAgent (por defecto true, con app-switch).
  • Botones de variables para saludo y despedida: {{greetings}}, {{username}}, {{fullName}}, {{phone}}, {{identifier}}, {{email}}, que se insertan en la posición del cursor.
  • Guardar: updateConnection(connection._id, {...}) (PATCH /connections/update/:id) y cierra con 'saved'. Al cerrar o descartar, la tabla recarga la lista (modalRef.closed y modalRef.dismissed).

La conexión se persiste en el backend antes de este formulario (el callback la crea o actualiza); si el usuario descarta el modal, la conexión queda creada pero sin departamentos. A diferencia del modal de WhatsApp, este modal no elimina la conexión al cerrarse.

Aviso de expiración​

ConnectionsTableComponent.instagramExpiry(connection) devuelve { days } solo si:

  • el canal es instagram,
  • metaConfig.expiresAt existe y la conexión está connected,
  • y los días restantes (Math.ceil((expiresAt - ahora) / 1 día)) son menores o iguales que INSTAGRAM_EXPIRY_WARNING_DAYS (7). El valor mostrado es Math.max(days, 1).

La tarjeta pinta entonces una franja connection-card__warning con connections.instagram.expiresSoon (texto en inglés: Instagram access expires in {{days}} days — reauthorize access, con el parámetro days) o expiresSoonOne cuando falta 1 día. El backend renueva automáticamente los tokens que vencen en los próximos 10 días (job horario MetaTokenRefreshJob); como el umbral del frontend (7 días) es menor que el del job, el aviso solo debería verse si esa renovación no se produjo. Si Graph rechaza el token (error 190) el backend marca metaConfig.authErrorAt y la conexión pasa a disconnected, y la tarjeta ofrece "Reautorizar" como acción principal.

Reautorización​

"Reautorizar" llama de nuevo a authorizeInstagram({ connectionId }). El backend limpia authErrorAt y deja la conexión en connected si el usuario autoriza la misma cuenta; con otra cuenta responde reason=account_mismatch. Nota: en instagram el menú ofrece "Reautorizar" siempre (permiso CONNECTION_CREATE), mientras que el botón principal de la tarjeta alterna entre "Editar" (conectada) y "Reautorizar" (no conectada).

Seguridad​

  • El frontend no ve ni almacena el token de Instagram ni el state del OAuth: solo recibe la URL de autorización y, al volver, meta, reason y connectionId.
  • Los query params de retorno se tratan como datos no confiables: el reason solo se usa como sufijo de clave i18n si pertenece a la lista cerrada META_OAUTH_REASONS, y el connectionId solo se usa para GET /connections/get-one/:id, endpoint protegido por CONNECTION_READ en el backend.