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
- Inicio. El usuario elige "Instagram" en
ConnectionChannelModalComponent(alta) o pulsa "Reautorizar" en una tarjeta existente. Ambos llaman aConnectionsTableComponent.connectInstagram(connection?). - Autorización.
connectInstagramponeisAuthorizingInstagram = true(evita dobles clics y deshabilita "Reautorizar") y llama aConnectionService.authorizeInstagram(connection ? { connectionId } : {}), es decirPOST <apiUrl>/meta/oauth/instagram/authorize. El backend responde{ data: { url } }. - Salto a Instagram. El frontend hace
window.location.href = url(navegación completa, no popup). Si falla la petición, se restableceisAuthorizingInstagram; si el backend responde conerror.data.code === 'META_NOT_CONFIGURED'se muestra el toastconnections.instagram.errors.notConfigured("Instagram no está habilitado en este servidor").ErrorInterceptordeja pasar los errores condata.codesin toast propio, por eso el componente los trata. - Retorno. El backend redirige a
/connections?meta=....ngOnInitllama ahandleMetaOAuthReturn(), que lee los query params y los limpia conrouter.navigate([], { relativeTo, queryParams: {}, replaceUrl: true })para que recargar no repita el aviso. - Resultado.
meta=connectedconconnectionId: toast de éxito (connections.instagram.connected),findConnectionById(connectionId)y apertura del modal de configuración conjustConnected = true.- Otro caso: toast de error con la clave
connections.instagram.errors.<reason>. Sireasonno está enMETA_OAUTH_REASONSse usaunknown.
Constantes (connections.constants.ts)
| Constante | Valor | Uso |
|---|---|---|
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_REASONS | denied, invalid_state, exchange_failed, subscription_failed, account_in_use, account_mismatch, connection_not_found | Razones conocidas |
META_NOT_CONFIGURED_CODE | 'META_NOT_CONFIGURED' | Código de error del backend si falta configuración de Meta |
INSTAGRAM_EXPIRY_WARNING_DAYS | 7 | Umbral del aviso de expiración |
Mensajes de error (connections.instagram.errors.*)
| Clave | Mensaje (en) |
|---|---|
notConfigured | Instagram is not enabled on this server. |
denied | You cancelled the authorization on Instagram. |
invalid_state | The authorization expired or is not valid. Try again. |
exchange_failed | Instagram did not confirm the authorization. Try again. |
subscription_failed | Message delivery could not be enabled for this account. Try again. |
account_in_use | This Instagram account is already connected to another company. |
account_mismatch | You authorized a different account than this connection's. Log in with the right account. |
connection_not_found | The connection you wanted to reauthorize no longer exists. |
unknown | Instagram could not be connected. Try again. |
El archivo es.json contiene las mismas claves en español.
Modal de configuración
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" cuandometaConfig.accountNameexiste. - Con
justConnected = truemuestra 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 conng-select; los departamentos vienen deDepartmentService.getAll()),reopenToleranceMinutes(obligatorio, mínimo 1, por defecto 15) yreturnToLastAgent(por defectotrue, conapp-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.closedymodalRef.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.expiresAtexiste y la conexión estáconnected,- y los días restantes (
Math.ceil((expiresAt - ahora) / 1 día)) son menores o iguales queINSTAGRAM_EXPIRY_WARNING_DAYS(7). El valor mostrado esMath.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
statedel OAuth: solo recibe la URL de autorización y, al volver,meta,reasonyconnectionId. - Los query params de retorno se tratan como datos no confiables: el
reasonsolo se usa como sufijo de clave i18n si pertenece a la lista cerradaMETA_OAUTH_REASONS, y elconnectionIdsolo se usa paraGET /connections/get-one/:id, endpoint protegido porCONNECTION_READen el backend.