services/api/src/modules/auth/phone-verification.controller.js

Overview

Este archivo define los handlers HTTP de verificación telefónica dentro del módulo auth. Expone dos operaciones: iniciar la verificación y confirmar el código recibido por SMS. Ambos handlers validan el cuerpo, llaman al servicio de dominio y devuelven respuestas JSON mínimas. El router del módulo los monta en POST /auth/phone/verify/start y POST /auth/phone/verify/check, siempre detrás de requireAuth .

La intención del archivo es mantener la capa HTTP delgada. La lógica real de negocio vive en phone-verification.service.js, mientras que la validación de entrada vive en auth.validation.js fileciteturn0file2turn0file9.

[!NOTE] Este controlador no habla con Twilio directamente. Solo adapta la petición HTTP al servicio de dominio, que a su vez usa phone-verification.client.js fileciteturn2file2turn0file1.

How it works

flowchart TB
    Client[Authenticated client] --> StartRoute[POST phone verify start]
    Client --> CheckRoute[POST phone verify check]

    StartRoute --> StartHandler[startPhoneVerificationRequest]
    StartHandler --> StartValidation[validatePhoneVerificationStartInput]
    StartValidation --> StartService[startPhoneVerificationForUser]
    StartService --> StartResponse[200 alreadyVerified or SMS message]

    CheckRoute --> CheckHandler[checkPhoneVerificationRequest]
    CheckHandler --> CheckValidation[validatePhoneVerificationCheckInput]
    CheckValidation --> CheckService[checkPhoneVerificationForUser]
    CheckService --> CheckResponse[200 message and phoneVerifiedAt]

El flujo es lineal en ambos endpoints. Primero se valida req.body, después se llama al servicio con req.auth.userId, y al final se responde con JSON. Si algo falla, el handler entrega el error a next(error) para que lo resuelva el middleware global.

El contrato HTTP de este módulo está documentado como parte de la verificación de teléfono. Los tests cubren exactamente POST /auth/phone/verify/start y POST /auth/phone/verify/check .

Key components

{
    "title": "startPhoneVerificationRequest",
    "content": "Handler p\u00fablico que valida `req.body`, llama a `startPhoneVerificationForUser(req.auth.userId, input.phone)` y responde con un JSON m\u00ednimo. Devuelve `alreadyVerified` y un mensaje fijo.",
    "type": "function",
    "filePath": "services/api/src/modules/auth/phone-verification.controller.js",
    "badges": [
        "public"
    ]
}
{
    "title": "checkPhoneVerificationRequest",
    "content": "Handler p\u00fablico que valida `req.body`, llama a `checkPhoneVerificationForUser(req.auth.userId, input)` y responde con el mensaje de confirmaci\u00f3n y `phoneVerifiedAt`.",
    "type": "function",
    "filePath": "services/api/src/modules/auth/phone-verification.controller.js",
    "badges": [
        "public"
    ]
}

Data & fields

Contexto de autenticación

[
    {
        "name": "req.auth.userId",
        "type": "string",
        "required": true,
        "description": "Identificador del usuario autenticado. El router lo prepara con `requireAuth` antes de llegar al controlador."
    }
]

[!IMPORTANT] Este controlador asume una sesión válida. auth.routes.js coloca requireAuth antes de ambos handlers, así que req.auth.userId ya debe existir cuando entran aquí .

Body de startPhoneVerificationRequest

[
    {
        "name": "phone",
        "type": "string",
        "required": true,
        "description": "Número de teléfono en formato E.164. El controlador lo recibe ya validado por `validatePhoneVerificationStartInput`."
    }
]

Respuesta de startPhoneVerificationRequest

[
    {
        "name": "alreadyVerified",
        "type": "boolean",
        "required": true,
        "description": "Indica si el usuario pidió verificar exactamente su propio número ya verificado."
    },
    {
        "name": "message",
        "type": "string",
        "required": true,
        "description": "Mensaje breve para la UI. El texto cambia solo en el caso ya verificado."
    }
]
  • Si el número coincide con el número ya verificado del propio usuario, la respuesta es:
    • alreadyVerified: true
    • message: "Este numero ya esta verificado en tu cuenta."
  • En cualquier otro caso, la respuesta es:
    • alreadyVerified: false
    • message: "Te enviamos un codigo de verificacion por SMS."

Este comportamiento viene del servicio, no del controlador. El controlador solo serializa el resultado fileciteturn0file2turn0file3.

Body de checkPhoneVerificationRequest

[
    {
        "name": "phone",
        "type": "string",
        "required": true,
        "description": "Número de teléfono en formato E.164. El controlador lo recibe ya normalizado y validado."
    },
    {
        "name": "code",
        "type": "string",
        "required": true,
        "description": "Código recibido por SMS. No puede venir vacío."
    }
]

Respuesta de checkPhoneVerificationRequest

[
    {
        "name": "message",
        "type": "string",
        "required": true,
        "description": "Mensaje de éxito para la interfaz."
    },
    {
        "name": "phoneVerifiedAt",
        "type": "string",
        "required": true,
        "description": "Fecha de verificación devuelta por el servicio. En JSON llega serializada como fecha."
    }
]

Behaviors

  • startPhoneVerificationRequest — valida req.body, llama al servicio con req.auth.userId y responde 200 con una forma mínima.
  • startPhoneVerificationRequest — siempre delega los errores a next(error).
  • checkPhoneVerificationRequest — valida req.body, llama al servicio con req.auth.userId y devuelve phoneVerifiedAt.
  • checkPhoneVerificationRequest — siempre delega los errores a next(error).

El patrón es deliberadamente simple. La validación de formato está en auth.validation.js, y la lógica de persistencia está en phone-verification.service.js fileciteturn0file9turn0file3.

API

POST /auth/phone/verify/start

Este endpoint inicia la verificación SMS para un teléfono autenticado. El router lo protege con requireAuth, y el controlador solo devuelve una respuesta mínima. La forma de 200 cambia según alreadyVerified fileciteturn1file2turn0file2turn0file3.

{
    "title": "Start phone verification",
    "description": "Inicia la verificaci\u00f3n de tel\u00e9fono para el usuario autenticado. Si el n\u00famero ya es el propio n\u00famero verificado del usuario, devuelve `alreadyVerified: true`. En cualquier otro caso, dispara el flujo SMS y devuelve `alreadyVerified: false`.",
    "method": "POST",
    "baseUrl": "http://localhost:3000/api/auth",
    "endpoint": "/phone/verify/start",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"phone\": \"+525512345678\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Solicitud procesada correctamente.",
            "body": "{\n  \"alreadyVerified\": false,\n  \"message\": \"Te enviamos un codigo de verificacion por SMS.\"\n}"
        },
        "400": {
            "description": "El tel\u00e9fono no cumple el formato esperado.",
            "body": "{\n  \"message\": \"El telefono debe usar formato internacional E.164\"\n}"
        }
    }
}

POST /auth/phone/verify/check

Este endpoint confirma el código recibido por SMS. El controlador valida el cuerpo y devuelve la fecha de verificación cuando el servicio aprueba el código. La respuesta 200 incluye el mensaje de éxito y phoneVerifiedAt fileciteturn0file2turn0file3.

{
    "title": "Check phone verification",
    "description": "Confirma el c\u00f3digo SMS para el usuario autenticado. Si el servicio aprueba la verificaci\u00f3n, el backend devuelve la fecha `phoneVerifiedAt`.",
    "method": "POST",
    "baseUrl": "http://localhost:3000/api/auth",
    "endpoint": "/phone/verify/check",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "application/json",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"phone\": \"+525512345678\",\n  \"code\": \"123456\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Tel\u00e9fono verificado correctamente.",
            "body": "{\n  \"message\": \"Telefono verificado correctamente.\",\n  \"phoneVerifiedAt\": \"2026-02-01T00:00:00.000Z\"\n}"
        },
        "400": {
            "description": "El tel\u00e9fono o el c\u00f3digo no son v\u00e1lidos, o el c\u00f3digo no fue aprobado por el proveedor.",
            "body": "{\n  \"message\": \"El codigo no es valido o ya expiro\"\n}"
        }
    }
}

Gotchas & edge cases

[!WARNING] Este controlador no expone metadatos de Twilio. No devuelve SID, canal ni respuesta cruda del proveedor. Solo expone alreadyVerified, message y phoneVerifiedAt fileciteturn0file2turn2file2.

[!WARNING] alreadyVerified no es decorativo. Solo vale true cuando el usuario reingresa su propio número ya verificado. Para cualquier otro caso, el valor es false y el mensaje es el mismo fileciteturn0file3turn2file8.

[!WARNING] req.auth.userId debe existir antes de entrar al handler. Esa garantía la da requireAuth en auth.routes.js, no este archivo .

[!NOTE] La validación del teléfono y del código no vive aquí. validatePhoneVerificationStartInput y validatePhoneVerificationCheckInput están en auth.validation.js .

Este archivo se integra con varias piezas del mismo módulo. auth.routes.js lo monta en las rutas HTTP protegidas. auth.validation.js valida los cuerpos de entrada. phone-verification.service.js ejecuta la lógica de dominio. phone-verification.client.js encapsula el SDK de Twilio. Y services/api/test/phone-verification.test.js verifica el contrato completo de POST /auth/phone/verify/start y POST /auth/phone/verify/check fileciteturn1file2turn0file9turn0file3turn2file2turn2file0.

[!IMPORTANT] El alcance de este archivo es HTTP. La persistencia, la normalización de teléfono y la resolución de conflictos viven más abajo, en el servicio de dominio.