services/api/src/modules/contact-gate/contact-gate.controller.js
Overview
Este archivo define los dos handlers HTTP del módulo contact-gate. Ambos trabajan sobre una sesión autenticada y se montan bajo /api/contact-gate desde services/api/src/app.js. El router aplica requireAuth antes de los rate limiters, y el negocio real vive en contact-gate.service.js.
El propósito del módulo es proteger la acción de contactar a un anunciante. Según el servicio y la documentación del backend, esto cubre anti-automatización y scraping sobre el contacto, no verificación de identidad, teléfono o reputación.
[!IMPORTANT] Estas rutas siempre requieren sesión. El
requireAuthdel router se ejecuta antes que los limitadores por usuario, porque suskeyGeneratorleenreq.auth.userId.
How it works
flowchart TD
Client[Cliente autenticado]
RequireAuth[requireAuth]
ChallengeLimit[challengeRateLimit]
ChallengeCtl[contactGateAltchaChallenge]
AltchaGen[generateAltchaChallenge]
AuthorizeLimit[authorizeRateLimit]
AuthorizeCtl[contactGateAuthorize]
AuthorizeSvc[authorizeContactIntent]
UserGate[User contactGate]
VerifyAltcha[verifyAndConsumeAltchaPayload]
Client -->|GET contact gate challenge| RequireAuth
RequireAuth --> ChallengeLimit
ChallengeLimit --> ChallengeCtl
ChallengeCtl --> AltchaGen
AltchaGen -->|challenge JSON| ChallengeCtl
ChallengeCtl -->|200 JSON| Client
Client -->|POST contact gate authorize| RequireAuth
RequireAuth --> AuthorizeLimit
AuthorizeLimit --> AuthorizeCtl
AuthorizeCtl --> AuthorizeSvc
AuthorizeSvc -->|consume existing window| UserGate
AuthorizeSvc -->|validate new payload| VerifyAltcha
VerifyAltcha --> AuthorizeSvc
AuthorizeSvc -->|200 JSON| AuthorizeCtl
AuthorizeCtl --> Client
La ruta GET /altcha-challenge genera un challenge ALTCHA ligado al usuario autenticado. La ruta POST /authorize intenta consumir una ventana vigente en User.contactGate, o valida un altchaPayload nuevo cuando la ventana ya no existe. Si el payload no es string, el controlador lo ignora y pasa undefined al servicio.
El controlador no guarda estado propio. Solo adapta la petición HTTP, llama a la capa de servicio o al helper ALTCHA, y delega errores a next(error). El resultado final vuelve como JSON con estado 200.
Key components
El archivo contiene dos handlers públicos. Ambos son funciones async pensadas para Express, con manejo explícito de errores mediante try/catch. También dependen de dos capas externas ya existentes en el módulo.
{
"title": "contactGateAltchaChallenge",
"content": "Handler p\u00fablico que genera un challenge ALTCHA ligado al usuario autenticado y responde con JSON. Tambi\u00e9n aplica cabeceras de no cach\u00e9.",
"type": "function",
"filePath": "services/api/src/modules/contact-gate/contact-gate.controller.js",
"badges": [
"public"
]
}
{
"title": "contactGateAuthorize",
"content": "Handler p\u00fablico que autoriza una intenci\u00f3n de contacto. Normaliza `altchaPayload`, llama al servicio y devuelve el resultado como JSON.",
"type": "function",
"filePath": "services/api/src/modules/contact-gate/contact-gate.controller.js",
"badges": [
"public"
]
}
{
"title": "authorizeContactIntent",
"content": "Funci\u00f3n de servicio que decide si la intenci\u00f3n de contacto ya est\u00e1 autorizada o si necesita un nuevo payload ALTCHA.",
"type": "function",
"filePath": "services/api/src/modules/contact-gate/contact-gate.service.js",
"badges": [
"public"
]
}
{
"title": "generateAltchaChallenge",
"content": "Helper compartido que construye el challenge ALTCHA usado por el gate de contacto.",
"type": "function",
"filePath": "services/api/src/config/altcha.js",
"badges": [
"public"
]
}
Data & fields
El controlador solo consume unos pocos datos explícitos. La respuesta final de autorización sí tiene una forma concreta, definida por el servicio. El challenge devuelto por ALTCHA se trata como un objeto opaco en este archivo.
Entrada del request
[
{
"name": "req.auth.userId",
"type": "string",
"required": true,
"description": "Identificador del usuario autenticado. Se usa para ligar el challenge y la autorización."
},
{
"name": "req.body.altchaPayload",
"type": "string",
"description": "Payload ALTCHA enviado por el cliente. Si no es string, el controlador lo convierte en undefined."
}
]
Resultado de autorización
[
{
"name": "authorized",
"type": "boolean",
"required": true,
"description": "Indica que la intención de contacto quedó autorizada."
},
{
"name": "remaining",
"type": "number",
"required": true,
"description": "Intentos restantes dentro de la ventana vigente."
},
{
"name": "resetAt",
"type": "string",
"required": true,
"description": "Marca temporal asociada a la ventana vigente."
}
]
Constantes del gate
CONTACT_GATE_PURPOSE[badge:read-only] ='contact-intent'— propósito ALTCHA usado en este flujo.CONTACT_GATE_MAX_COUNT[badge:read-only] =15— máximo de intentos autorizados dentro de una ventana.CONTACT_GATE_WINDOW_MS[badge:read-only] =24 * 60 * 60 * 1000— ventana de 24 horas desde la última resolución ALTCHA.
Behaviors
El archivo implementa dos comportamientos HTTP claros. Ambos siguen el mismo patrón: leer entrada mínima, delegar, responder con 200, y pasar errores al middleware siguiente.
contactGateAltchaChallenge[badge:public] — Llama agenerateAltchaChallengeconpurpose: CONTACT_GATE_PURPOSEyuserId: req.auth.userId. Después fijaCache-ControlyPragmapara evitar caché, y devuelve el challenge como JSON.contactGateAuthorize[badge:public] — Leereq.body.altchaPayloadsolo si es string. Luego llama aauthorizeContactIntent(req.auth.userId, altchaPayload)y devuelve el resultado como JSON.
API
El router expone estas rutas bajo /api/contact-gate. El montaje ocurre en services/api/src/app.js, y el módulo aplica autenticación antes de ambos endpoints. El backend y las pruebas confirman además los límites por usuario.
GET /contact-gate/altcha-challenge
{
"title": "GET contact-gate altcha challenge",
"description": "Devuelve un challenge ALTCHA ligado al usuario autenticado y al prop\u00f3sito contact-intent.",
"method": "GET",
"baseUrl": "/api",
"endpoint": "/contact-gate/altcha-challenge",
"headers": [
{
"key": "Authorization",
"value": "Bearer <token>",
"required": true
}
],
"queryParams": [],
"pathParams": [],
"bodyType": "none",
"requestBody": "",
"formData": [],
"rawBody": "",
"responses": {
"200": {
"description": "Challenge ALTCHA emitido.",
"body": "{\n \"parameters\": {\n \"algorithm\": \"PBKDF2/SHA-256\"\n }\n}"
},
"401": {
"description": "Sesi\u00f3n requerida.",
"body": "{\n \"error\": \"Unauthorized\"\n}"
},
"429": {
"description": "L\u00edmite por usuario superado.",
"body": "{\n \"error\": \"Demasiadas solicitudes de verificacion. Intenta mas tarde.\"\n}"
}
}
}
POST /contact-gate/authorize
{
"title": "POST contact-gate authorize",
"description": "Autoriza una intenci\u00f3n de contacto consumiendo una ventana vigente o validando un payload ALTCHA nuevo.",
"method": "POST",
"baseUrl": "/api",
"endpoint": "/contact-gate/authorize",
"headers": [
{
"key": "Authorization",
"value": "Bearer <token>",
"required": true
},
{
"key": "Content-Type",
"value": "application/json",
"required": true
}
],
"queryParams": [],
"pathParams": [],
"bodyType": "json",
"requestBody": "{\n \"altchaPayload\": \"<base64 altcha payload>\"\n}",
"formData": [],
"rawBody": "",
"responses": {
"200": {
"description": "Intenci\u00f3n autorizada.",
"body": "{\n \"authorized\": true,\n \"remaining\": 14,\n \"resetAt\": \"2026-01-01T00:00:00.000Z\"\n}"
},
"400": {
"description": "Falta el payload ALTCHA. Tambi\u00e9n cubre payloads de otro prop\u00f3sito o ya consumidos.",
"body": "{\n \"error\": \"Se requiere completar la verificacion ALTCHA para continuar.\",\n \"code\": \"ALTCHA_REQUIRED\",\n \"field\": \"altchaPayload\"\n}"
},
"401": {
"description": "Sesi\u00f3n requerida.",
"body": "{\n \"error\": \"Unauthorized\"\n}"
},
"429": {
"description": "L\u00edmite por usuario superado.",
"body": "{\n \"error\": \"Demasiados intentos de contacto. Intenta mas tarde.\"\n}"
}
}
}
Workflows
El flujo principal tiene dos caminos. Uno emite el challenge y el otro consume o abre la ventana de contacto. Las pruebas de integración confirman ambos recorridos y sus límites.
- Emisión de challenge —
GET /contact-gate/altcha-challengedevuelve un challenge ALTCHA ligado al usuario y responde sin caché. - Autorización de contacto —
POST /contact-gate/authorizeusa una ventana vigente si existe; si no, exigealtchaPayload.
Gotchas & edge cases
[!WARNING]
contactGateAuthorizesolo aceptaaltchaPayloadcuando es una cadena. Cualquier otro tipo se convierte enundefinedy el servicio sigue la rama de validación.
[!WARNING] Si no existe una ventana vigente y falta
altchaPayload, el servicio lanzaApiErrorcon estado400. El código de negocio esALTCHA_REQUIRED, y el campo marcado esaltchaPayload.
[!CAUTION] El challenge de contacto no es reutilizable para otros propósitos. Las pruebas confirman que un challenge de otro
purposese rechaza, y que un payload ya consumido también falla con400.
[!IMPORTANT] Las respuestas del challenge no deben cachearse. El controlador fuerza
Cache-Control: no-store, no-cache, must-revalidate, privateyPragma: no-cache.
[!NOTE] El router usa límites por usuario distintos para cada endpoint.
GET /altcha-challengetiene 30 solicitudes por 5 minutos, yPOST /authorizetiene 60 por 15 minutos.
Related
Este controlador se relaciona directamente con varios archivos del mismo módulo. El router define el montaje y los límites; el servicio ejecuta la regla de negocio; y altcha.js provee la infraestructura común de challenge y verificación. El app.js monta todo el módulo en la API.
services/api/src/modules/contact-gate/contact-gate.routes.js— defineGET /altcha-challengeyPOST /authorize, aplicarequireAuthy rate limiting por usuario.services/api/src/modules/contact-gate/contact-gate.service.js— contieneauthorizeContactIntenty las constantes del gate.services/api/src/config/altcha.js— exponegenerateAltchaChallengeyverifyAndConsumeAltchaPayload, compartidos con otros flujos ALTCHA.services/api/src/app.js— monta el módulo bajo/api/contact-gate.services/api/test/contact-gate.test.js— cubre autenticación, validación ALTCHA, ventana de 15 intentos y expiración a 24 horas.services/api/test/contact-gate-rate-limits.test.js— verifica los límites HTTP reales de 30/5min y 60/15min.