services/api/src/modules/auth/auth.controller.js

Overview

Este archivo concentra la capa HTTP del módulo de autenticación. Toma datos desde req.body, req.query y req.auth.userId, valida la entrada cuando corresponde, y delega el trabajo real en auth.service.js, account-export.service.js y ../../config/altcha.js. También fija códigos HTTP, cabeceras de caché y el formato final de la respuesta.

El controlador se monta bajo /api/auth desde services/api/src/app.js, y el router aplica requireAuth en los flujos protegidos. Eso hace que varios handlers reciban req.auth.userId ya resuelto antes de entrar al controlador.

[!NOTE] Este archivo mezcla respuestas JSON, HTML y binarios ZIP. El flujo de cancelación de digest devuelve HTML directo, y la exportación de cuenta responde con un archivo ZIP en memoria.

How it works

flowchart TB
    Client[Cliente HTTP] --> Routes[auth routes]
    Routes --> Controller[auth.controller.js]

    Controller --> Validation[auth.validation.js]
    Controller --> AuthService[auth.service.js]
    Controller --> Altcha[altcha helpers]
    Controller --> ExportSvc[account export service]
    Controller --> Errors[ApiError]

    Controller --> Json[JSON response]
    Controller --> Html[HTML response]
    Controller --> Zip[ZIP response]

El flujo general es consistente en casi todos los handlers. Primero se valida el cuerpo o el token recibido. Después se llama a un servicio concreto y se serializa la respuesta final. Los casos especiales son los endpoints de ALTCHA, la exportación de cuenta y la cancelación de digest, porque devuelven challenge, ZIP o HTML.

[!IMPORTANT] El flujo de cancelación de digest tiene dos fases distintas. El GET solo valida el token y muestra un formulario. El POST realiza la mutación real sobre notificationPreferences.listingPerformanceDigest.

Key components

Este controlador depende de validadores, servicios de dominio y helpers de seguridad. La mayor parte del archivo es orquestación, no lógica de negocio propia.

{
    "title": "generateAltchaChallenge",
    "content": "Crea el challenge ALTCHA que el controlador devuelve en los endpoints p\u00fablicos y protegidos de challenge.",
    "type": "function",
    "filePath": "services/api/src/config/altcha.js",
    "badges": [
        "public"
    ]
}
{
    "title": "verifyAndConsumeAltchaPayload",
    "content": "Verifica y consume el payload ALTCHA antes de aceptar registro, eliminaci\u00f3n de cuenta o restauraci\u00f3n.",
    "type": "function",
    "filePath": "services/api/src/config/altcha.js",
    "badges": [
        "public"
    ]
}
{
    "title": "verifyAccountExportRequest",
    "content": "Valida el token de exportaci\u00f3n y resuelve el usuario al que pertenece la descarga.",
    "type": "function",
    "filePath": "services/api/src/modules/auth/account-export.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "buildAccountExportZip",
    "content": "Construye en memoria el ZIP que contiene la exportaci\u00f3n de cuenta.",
    "type": "function",
    "filePath": "services/api/src/modules/auth/account-export.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "ApiError",
    "content": "Representa errores HTTP con estado, mensaje y metadatos adicionales.",
    "type": "class",
    "filePath": "services/api/src/utils/apiError.js",
    "badges": [
        "public"
    ]
}
{
    "title": "digestUnsubscribeConfirmHtml",
    "content": "Genera el HTML de confirmaci\u00f3n que se muestra antes de cancelar Tu Plano Digest.",
    "type": "function",
    "filePath": "services/api/src/modules/auth/auth.controller.js",
    "badges": [
        "public"
    ]
}

Validadores usados por el controlador

El archivo también depende de validadores específicos antes de llamar a los servicios. Esos validadores definen la forma final de cada payload de entrada.

  • validateRegisterInput
  • validateSignInInput
  • validateGoogleAuthInput
  • validateGoogleAccountDeletionReauthInput
  • validatePasswordCreateInput
  • validateDeleteAccountInput
  • validateRestoreAccountInput
  • validateAccountRestorationResendInput

Data & fields

Este controlador no define un modelo persistente propio. Solo consume datos de entrada y usa constantes HTML internas para la ruta de cancelación de digest.

[
    {
        "name": "req.auth.userId",
        "type": "string",
        "required": true,
        "description": "Identifica al usuario autenticado en los handlers protegidos."
    },
    {
        "name": "req.body",
        "type": "object",
        "required": true,
        "description": "Cuerpo de entrada que cada validador normaliza antes de delegar al servicio."
    },
    {
        "name": "req.query.token",
        "type": "string",
        "description": "Token usado en restauración, exportación y cancelación de digest."
    },
    {
        "name": "altchaPayload",
        "type": "string",
        "required": true,
        "description": "Prueba ALTCHA que se verifica y consume en registro, eliminación y restauración."
    },
    {
        "name": "credential",
        "type": "string",
        "required": true,
        "description": "Credencial de Google enviada por el cliente."
    },
    {
        "name": "email",
        "type": "string",
        "required": true,
        "description": "Correo usado para recuperar contraseña o reenviar restauración."
    },
    {
        "name": "token",
        "type": "string",
        "required": true,
        "description": "Token usado en reset, verificación, restauración, exportación y cancelación."
    },
    {
        "name": "currentPassword",
        "type": "string",
        "required": true,
        "description": "Contraseña actual usada en cambio de contraseña o eliminación de cuenta."
    },
    {
        "name": "newPassword",
        "type": "string",
        "required": true,
        "description": "Nueva contraseña usada en el cambio de contraseña."
    },
    {
        "name": "googleReauthToken",
        "type": "string",
        "description": "Token de reautenticación de uso único emitido por un flujo de Google."
    },
    {
        "name": "deletionGracePreferences",
        "type": "object",
        "description": "Preferencias de gracia que se pasan al flujo de eliminación de cuenta."
    },
    {
        "name": "DIGEST_UNSUBSCRIBE_SUCCESS_HTML",
        "type": "string",
        "required": true,
        "description": "HTML devuelto tras cancelar correctamente Tu Plano Digest."
    },
    {
        "name": "DIGEST_UNSUBSCRIBE_ERROR_HTML",
        "type": "string",
        "required": true,
        "description": "HTML devuelto cuando el token de cancelación falta o es inválido."
    }
]

Behaviors

  • altchaChallenge [badge:public] — genera un challenge ALTCHA genérico en JSON y marca la respuesta como no cacheable.
  • accountDeletionAltchaChallenge [badge:public] — genera un challenge ALTCHA ligado a account-deletion y al usuario autenticado.
  • register [badge:public] — valida el alta, consume altchaPayload, crea el usuario y responde 201.
  • signIn [badge:public] — valida credenciales y responde 200 con el resultado del inicio de sesión.
  • googleAuth [badge:public] — valida credential y delega en el flujo de Google para iniciar sesión o registrar.
  • googleAccountDeletionReauth [badge:public] — emite un reauth de Google de un solo uso para la eliminación de cuenta.
  • me [badge:public] — devuelve el usuario actual usando req.auth.userId.
  • updateMe [badge:public] — actualiza el perfil del usuario autenticado y devuelve el usuario actualizado.
  • forgotPassword [badge:public] — solicita un reset de contraseña y devuelve un mensaje neutral.
  • resetPassword [badge:public] — valida token y password, cambia la contraseña y confirma la operación.
  • verifyEmailAddress [badge:public] — verifica el correo con un token y devuelve el resultado del servicio.
  • resendVerification [badge:public] — reenvía el correo de verificación usando la identidad de la sesión.
  • changePassword [badge:public] — valida contraseña actual y nueva, exige una longitud mínima y cambia la credencial.
  • googlePasswordCreateReauth [badge:public] — emite un reauth de Google de un solo uso para crear una contraseña local.
  • createPassword [badge:public] — crea una contraseña para una cuenta Google-only.
  • deleteMe [badge:public] — verifica ALTCHA, valida credenciales y solicita la eliminación de la cuenta.
  • restoreAccountAltchaChallenge [badge:public] — valida un token de restauración y genera un challenge ALTCHA ligado al usuario.
  • restoreAccount [badge:public] — valida restauración y ALTCHA, restaura la cuenta y devuelve el resultado.
  • resendAccountRestoration [badge:public] — reenvía el enlace de restauración con una respuesta neutral.
  • downloadAccountExport [badge:public] — valida el token de exportación, genera un ZIP en memoria y lo devuelve como adjunto.
  • confirmListingPerformanceDigestUnsubscribe [badge:public] — valida el token de digest y devuelve el formulario HTML de confirmación.
  • unsubscribeListingPerformanceDigest [badge:public] — procesa el submit del formulario y desactiva solo listingPerformanceDigest.

Workflows

El archivo organiza tres grupos de flujos claros. Todos usan la misma frontera de controlador, pero cambian en autenticación, formato y persistencia.

  • Flujos públicos con token: forgotPassword, resetPassword, restoreAccountAltchaChallenge, restoreAccount, resendAccountRestoration, downloadAccountExport y confirmListingPerformanceDigestUnsubscribe.
  • Flujos autenticados: me, updateMe, changePassword, deleteMe, resendVerification, googleAccountDeletionReauth, googlePasswordCreateReauth y createPassword.
  • Flujos HTML o binarios: downloadAccountExport responde con ZIP, y los handlers de digest responden con HTML directo.

API

La superficie HTTP de este controlador vive en services/api/src/modules/auth/auth.routes.js y se monta bajo /api/auth. El router agrega requireAuth a los handlers protegidos y deja públicos los flujos basados en token o correo.

GET /auth/altcha/challenge

{
    "title": "Generar challenge ALTCHA",
    "description": "Crea un challenge ALTCHA gen\u00e9rico y devuelve una respuesta no cacheable.",
    "method": "GET",
    "baseUrl": "/api",
    "endpoint": "/auth/altcha/challenge",
    "headers": [],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Challenge generado",
            "body": "{\n  \"challenge\": \"<altcha challenge>\"\n}"
        }
    }
}

GET /auth/altcha/account-deletion-challenge

{
    "title": "Challenge ALTCHA para eliminaci\u00f3n",
    "description": "Genera un challenge ALTCHA ligado al usuario autenticado y al prop\u00f3sito account-deletion.",
    "method": "GET",
    "baseUrl": "/api",
    "endpoint": "/auth/altcha/account-deletion-challenge",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Challenge generado",
            "body": "{\n  \"challenge\": \"<altcha challenge>\"\n}"
        },
        "401": {
            "description": "Sesi\u00f3n inv\u00e1lida",
            "body": "{\n  \"message\": \"Token inv\u00e1lido o expirado\"\n}"
        }
    }
}

POST /auth/register

{
    "title": "Registrar usuario",
    "description": "Valida el cuerpo, consume el payload ALTCHA y crea una nueva cuenta.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/register",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"altchaPayload\": \"<payload>\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "201": {
            "description": "Usuario creado",
            "body": "{\n  \"token\": \"<jwt>\",\n  \"user\": {\n    \"_id\": \"<user-id>\",\n    \"email\": \"[email protected]\"\n  }\n}"
        },
        "400": {
            "description": "Solicitud inv\u00e1lida",
            "body": "{\n  \"message\": \"Datos de registro inv\u00e1lidos\"\n}"
        }
    }
}

POST /auth/sign-in

{
    "title": "Iniciar sesi\u00f3n",
    "description": "Valida credenciales y devuelve el resultado del login.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/sign-in",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"email\": \"[email protected]\",\n  \"password\": \"Secret123!\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Sesi\u00f3n iniciada",
            "body": "{\n  \"token\": \"<jwt>\",\n  \"user\": {\n    \"_id\": \"<user-id>\",\n    \"email\": \"[email protected]\"\n  }\n}"
        },
        "400": {
            "description": "Datos inv\u00e1lidos",
            "body": "{\n  \"message\": \"Credenciales inv\u00e1lidas\"\n}"
        }
    }
}

POST /auth/google

{
    "title": "Autenticaci\u00f3n con Google",
    "description": "Valida un credential de Google y delega en el flujo de inicio o registro.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/google",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"credential\": \"<google-id-token>\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Autenticaci\u00f3n completada",
            "body": "{\n  \"token\": \"<jwt>\",\n  \"user\": {\n    \"_id\": \"<user-id>\",\n    \"email\": \"[email protected]\"\n  }\n}"
        },
        "400": {
            "description": "Credential inv\u00e1lido",
            "body": "{\n  \"message\": \"Credencial de Google inv\u00e1lida\"\n}"
        }
    }
}

POST /auth/account-deletion/google-reauth

{
    "title": "Reauth de Google para eliminaci\u00f3n",
    "description": "Emite un token reauth de un solo uso para confirmar una eliminaci\u00f3n de cuenta.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/account-deletion/google-reauth",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"credential\": \"<google-id-token>\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Token reauth emitido",
            "body": "{\n  \"googleReauthToken\": \"<scoped-token>\"\n}"
        },
        "401": {
            "description": "Sesi\u00f3n inv\u00e1lida",
            "body": "{\n  \"message\": \"Token inv\u00e1lido o expirado\"\n}"
        }
    }
}

GET /auth/me

{
    "title": "Obtener usuario actual",
    "description": "Devuelve el usuario autenticado. La ruta requiere sesi\u00f3n activa.",
    "method": "GET",
    "baseUrl": "/api",
    "endpoint": "/auth/me",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Usuario actual",
            "body": "{\n  \"_id\": \"<user-id>\",\n  \"email\": \"[email protected]\",\n  \"name\": \"Ana Garc\u00eda\",\n  \"emailVerifiedAt\": null\n}"
        },
        "401": {
            "description": "No autenticado",
            "body": "{\n  \"message\": \"Token inv\u00e1lido o expirado\"\n}"
        }
    }
}

PATCH /auth/me

{
    "title": "Actualizar perfil actual",
    "description": "Actualiza el perfil del usuario autenticado y devuelve el recurso actualizado.",
    "method": "PATCH",
    "baseUrl": "/api",
    "endpoint": "/auth/me",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"name\": \"Ana Garc\u00eda\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Perfil actualizado",
            "body": "{\n  \"_id\": \"<user-id>\",\n  \"email\": \"[email protected]\",\n  \"name\": \"Ana Garc\u00eda\"\n}"
        },
        "401": {
            "description": "No autenticado",
            "body": "{\n  \"message\": \"Token inv\u00e1lido o expirado\"\n}"
        }
    }
}

DELETE /auth/me

{
    "title": "Solicitar eliminaci\u00f3n de cuenta",
    "description": "Verifica ALTCHA, valida credenciales y solicita la eliminaci\u00f3n de la cuenta autenticada.",
    "method": "DELETE",
    "baseUrl": "/api",
    "endpoint": "/auth/me",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"altchaPayload\": \"<payload>\",\n  \"currentPassword\": \"Secret123!\",\n  \"googleReauthToken\": \"<scoped-token>\",\n  \"deletionGracePreferences\": {}\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Solicitud registrada",
            "body": "{\n  \"message\": \"La eliminacion de la cuenta fue solicitada.\"\n}"
        },
        "401": {
            "description": "No autenticado",
            "body": "{\n  \"message\": \"Token inv\u00e1lido o expirado\"\n}"
        }
    }
}

GET /auth/restore-account/altcha-challenge

{
    "title": "Challenge de restauraci\u00f3n",
    "description": "Valida el token de restauraci\u00f3n y genera un challenge ALTCHA asociado al usuario restaurable.",
    "method": "GET",
    "baseUrl": "/api",
    "endpoint": "/auth/restore-account/altcha-challenge",
    "headers": [],
    "queryParams": [
        {
            "key": "token",
            "value": "Token de restauraci\u00f3n",
            "required": true
        }
    ],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Challenge generado",
            "body": "{\n  \"challenge\": \"<altcha challenge>\"\n}"
        },
        "400": {
            "description": "Token ausente o inv\u00e1lido",
            "body": "{\n  \"message\": \"El enlace de restauracion es invalido o ha expirado\"\n}"
        }
    }
}

POST /auth/restore-account

{
    "title": "Restaurar cuenta",
    "description": "Valida el token de restauraci\u00f3n y el payload ALTCHA, y restaura la cuenta.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/restore-account",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"token\": \"<restoration-token>\",\n  \"altchaPayload\": \"<payload>\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Cuenta restaurada",
            "body": "{\n  \"message\": \"Tu cuenta fue restaurada correctamente.\"\n}"
        },
        "400": {
            "description": "Token inv\u00e1lido o expirado",
            "body": "{\n  \"message\": \"El enlace de restauracion es invalido o ha expirado\"\n}"
        }
    }
}

POST /auth/account-restoration/resend

{
    "title": "Reenviar restauraci\u00f3n",
    "description": "Reenv\u00eda el enlace de restauraci\u00f3n con una respuesta neutral que no revela si la cuenta existe.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/account-restoration/resend",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"email\": \"[email protected]\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Solicitud aceptada",
            "body": "{\n  \"message\": \"Si la cuenta existe y est\u00e1 en proceso de eliminaci\u00f3n, enviamos un nuevo enlace de restauraci\u00f3n.\"\n}"
        },
        "400": {
            "description": "Correo faltante",
            "body": "{\n  \"message\": \"El correo es requerido\"\n}"
        }
    }
}

GET /auth/account-export

{
    "title": "Descargar exportaci\u00f3n de cuenta",
    "description": "Verifica el token de exportaci\u00f3n y devuelve un ZIP generado en memoria. No persiste archivos.",
    "method": "GET",
    "baseUrl": "/api",
    "endpoint": "/auth/account-export",
    "headers": [],
    "queryParams": [
        {
            "key": "token",
            "value": "Token de exportaci\u00f3n",
            "required": true
        }
    ],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "ZIP descargable",
            "body": "<binary ZIP attachment>"
        },
        "401": {
            "description": "Token inv\u00e1lido o expirado",
            "body": "{\n  \"message\": \"El enlace de exportacion no es valido o expiro\"\n}"
        }
    }
}

POST /auth/forgot-password

{
    "title": "Solicitar restablecimiento de contrase\u00f1a",
    "description": "Inicia el flujo de recuperaci\u00f3n. Devuelve un mensaje neutral y, fuera de producci\u00f3n, puede incluir devToken.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/forgot-password",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"email\": \"[email protected]\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Solicitud aceptada",
            "body": "{\n  \"message\": \"Si el correo existe, recibir\u00e1s instrucciones para recuperar tu contrase\u00f1a.\",\n  \"devToken\": \"<only-in-non-production>\"\n}"
        },
        "400": {
            "description": "Correo faltante",
            "body": "{\n  \"message\": \"El correo es requerido\"\n}"
        }
    }
}

POST /auth/reset-password

{
    "title": "Restablecer contrase\u00f1a",
    "description": "Valida el token de recuperaci\u00f3n y reemplaza la contrase\u00f1a.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/reset-password",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"token\": \"<reset-token>\",\n  \"password\": \"Secret123!\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Contrase\u00f1a actualizada",
            "body": "{\n  \"message\": \"Contrase\u00f1a actualizada correctamente\"\n}"
        },
        "400": {
            "description": "Token o contrase\u00f1a faltantes",
            "body": "{\n  \"message\": \"Token y contrase\u00f1a son requeridos\"\n}"
        }
    }
}

PATCH /auth/change-password

{
    "title": "Cambiar contrase\u00f1a",
    "description": "Requiere sesi\u00f3n activa y cambia la contrase\u00f1a tras validar la actual y la nueva.",
    "method": "PATCH",
    "baseUrl": "/api",
    "endpoint": "/auth/change-password",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"currentPassword\": \"Secret123!\",\n  \"newPassword\": \"Secret456!\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Contrase\u00f1a actualizada",
            "body": "{\n  \"message\": \"Contrase\u00f1a actualizada correctamente\"\n}"
        },
        "400": {
            "description": "Datos insuficientes o contrase\u00f1a demasiado corta",
            "body": "{\n  \"message\": \"La contrase\u00f1a actual y la nueva son requeridas\"\n}"
        }
    }
}

POST /auth/password/create/google-reauth

{
    "title": "Reauth para crear contrase\u00f1a",
    "description": "Emite un reauth de un solo uso para crear una contrase\u00f1a local en una cuenta Google-only.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/password/create/google-reauth",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"credential\": \"<google-id-token>\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Token reauth emitido",
            "body": "{\n  \"googleReauthToken\": \"<scoped-token>\"\n}"
        },
        "401": {
            "description": "Sesi\u00f3n inv\u00e1lida",
            "body": "{\n  \"message\": \"Token inv\u00e1lido o expirado\"\n}"
        }
    }
}

POST /auth/password/create

{
    "title": "Crear contrase\u00f1a local",
    "description": "Crea una contrase\u00f1a para una cuenta Google-only usando la sesi\u00f3n activa y el reauth previo.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/password/create",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"googleReauthToken\": \"<scoped-token>\",\n  \"password\": \"Secret123!\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Contrase\u00f1a creada",
            "body": "{\n  \"message\": \"Contrase\u00f1a creada correctamente\"\n}"
        },
        "400": {
            "description": "Datos inv\u00e1lidos",
            "body": "{\n  \"message\": \"Datos de contrase\u00f1a inv\u00e1lidos\"\n}"
        }
    }
}

POST /auth/verify-email

{
    "title": "Verificar correo",
    "description": "Verifica un token de email y devuelve el resultado de la verificaci\u00f3n.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/verify-email",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"token\": \"<verification-token>\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Correo verificado",
            "body": "{\n  \"message\": \"Correo verificado\"\n}"
        },
        "400": {
            "description": "Token faltante",
            "body": "{\n  \"message\": \"El token de verificacion es obligatorio\"\n}"
        }
    }
}

POST /auth/resend-verification

{
    "title": "Reenviar verificaci\u00f3n",
    "description": "Reenv\u00eda el correo de verificaci\u00f3n usando la identidad de la sesi\u00f3n.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/resend-verification",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Solicitud aceptada",
            "body": "{\n  \"message\": \"Te enviamos un nuevo enlace de verificaci\u00f3n.\"\n}"
        },
        "401": {
            "description": "Sesi\u00f3n inv\u00e1lida",
            "body": "{\n  \"message\": \"Token inv\u00e1lido o expirado\"\n}"
        }
    }
}

GET /auth/notification-preferences/digest/unsubscribe

{
    "title": "Confirmar cancelaci\u00f3n de digest",
    "description": "Valida el token y devuelve una p\u00e1gina HTML de confirmaci\u00f3n. No muta datos en GET.",
    "method": "GET",
    "baseUrl": "/api",
    "endpoint": "/auth/notification-preferences/digest/unsubscribe",
    "headers": [],
    "queryParams": [
        {
            "key": "token",
            "value": "Token de cancelaci\u00f3n de digest",
            "required": true
        }
    ],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Formulario de confirmaci\u00f3n",
            "body": "<html>...formulario de confirmaci\u00f3n...</html>"
        },
        "400": {
            "description": "Token faltante o inv\u00e1lido",
            "body": "<html>...enlace no v\u00e1lido...</html>"
        }
    }
}

POST /auth/notification-preferences/digest/unsubscribe

{
    "title": "Cancelar Tu Plano Digest",
    "description": "Procesa el submit del formulario y desactiva solo notificationPreferences.listingPerformanceDigest.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/auth/notification-preferences/digest/unsubscribe",
    "headers": [
        {
            "key": "Content-Type",
            "value": "application/x-www-form-urlencoded",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "form",
    "requestBody": "",
    "formData": [
        {
            "key": "token",
            "value": "Token de cancelaci\u00f3n de digest",
            "required": true
        }
    ],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Preferencia desactivada",
            "body": "<html>...confirmaci\u00f3n de cancelaci\u00f3n...</html>"
        },
        "400": {
            "description": "Token faltante o inv\u00e1lido",
            "body": "<html>...enlace no v\u00e1lido...</html>"
        }
    }
}

Gotchas & edge cases

[!WARNING] forgotPassword no revela si el correo existe. En producción devuelve solo el mensaje neutral. Fuera de producción, puede añadir devToken.

[!CAUTION] changePassword rechaza newPassword con menos de 8 caracteres antes de llamar al servicio. Esa validación ocurre en el controlador, no en el router.

[!IMPORTANT] confirmListingPerformanceDigestUnsubscribe no muta la base de datos. La mutación real solo ocurre en unsubscribeListingPerformanceDigest.

[!NOTE] downloadAccountExport usa un token de un solo propósito en req.query.token. El ZIP se arma en memoria y se devuelve como somos-plano-export.zip.

  • services/api/src/modules/auth/auth.routes.js monta todos los handlers de este archivo bajo /api/auth y aplica requireAuth en los flujos que lo necesitan.
  • services/api/src/modules/auth/auth.validation.js normaliza los payloads de registro, login, Google, reauth, creación de contraseña, eliminación, restauración y reenvío.
  • services/api/src/modules/auth/auth.service.js concentra la lógica de negocio que este controlador invoca.
  • services/api/src/modules/auth/account-export.service.js verifica el token de exportación y construye el ZIP en memoria.
  • services/api/src/config/altcha.js genera y consume los challenges usados por registro, eliminación y restauración.
  • services/api/src/utils/apiError.js provee los errores HTTP que el controlador lanza cuando faltan campos o tokens.