Rutas de uploads en services/api/src/modules/uploads/uploads.routes.js

Descripción general

Este archivo define el router HTTP de uploads. Expone dos rutas autenticadas para ImageKit y para subida de archivos. También conecta la validación de propiedad de anuncios con la carga de medios.

[!NOTE] Las rutas son relativas al router. El punto de montaje final no aparece en este archivo.

[!IMPORTANT] Ambas rutas usan requireAuth. Eso significa que req.auth.userId ya existe cuando entra al handler.

Cómo funciona

El archivo crea un express.Router() y un middleware de multer en memoria. Luego registra dos endpoints: uno genera parámetros de autenticación de ImageKit, y el otro recibe un archivo multipart y lo delega a uploadAssetToImageKit. Cuando listingId viene en la petición, la ruta valida que el anuncio pertenezca al usuario autenticado antes de subir el archivo.

flowchart TB
    Router[uploads routes router]

    subgraph AuthEndpoint[GET imagekit auth]
        AuthMW[requireAuth]
        ImageKitClient[getImageKitClient helper]
        PublicConfig[getImageKitPublicConfig helper]
        AuthResponse[JSON response]
        AuthMW --> ImageKitClient
        ImageKitClient --> AuthResponse
        AuthMW --> PublicConfig
        PublicConfig --> AuthResponse
    end

    subgraph UploadEndpoint[POST upload]
        Multer[multer memory storage]
        ListingCheck[Listing ownership check]
        ListingModel[Listing model]
        MediaService[uploadAssetToImageKit]
        UploadResponse[JSON response]
        Multer --> ListingCheck
        ListingCheck --> ListingModel
        ListingCheck --> MediaService
        MediaService --> UploadResponse
    end

    Router --> AuthMW
    Router --> Multer

Componentes clave

Este módulo mezcla enrutado HTTP, autenticación y control de carga. La lógica de negocio principal se delega a helpers y servicios importados.

  • router — instancia de express.Router() que agrupa las rutas de uploads.
  • upload — middleware de multer con memoryStorage() y límite de 25 MB.
  • Listing — modelo de Mongoose usado para validar ownership del anuncio.
  • mongoose.Types.ObjectId.isValid() — valida el formato de listingId antes de consultar la base de datos.
{
    "title": "requireAuth",
    "content": "Middleware de autenticaci\u00f3n usado en ambas rutas. Aporta acceso a `req.auth.userId` antes de ejecutar la l\u00f3gica del endpoint.",
    "type": "function",
    "filePath": "services/api/src/modules/auth/auth.middleware.js",
    "badges": [
        "public"
    ]
}
{
    "title": "getImageKitClient",
    "content": "Helper que devuelve el cliente de ImageKit. La ruta lo usa para generar par\u00e1metros de autenticaci\u00f3n sin exponer la clave privada.",
    "type": "function",
    "filePath": "services/api/src/config/imagekit.js",
    "badges": [
        "public"
    ]
}
{
    "title": "getImageKitPublicConfig",
    "content": "Helper que aporta la configuraci\u00f3n p\u00fablica de ImageKit. Sus valores se mezclan con los par\u00e1metros de autenticaci\u00f3n en la respuesta.",
    "type": "function",
    "filePath": "services/api/src/config/imagekit.js",
    "badges": [
        "public"
    ]
}
{
    "title": "uploadAssetToImageKit",
    "content": "Servicio que realiza la subida efectiva del archivo. Recibe el archivo, la carpeta solicitada, `listingId`, `userId` y etiquetas normalizadas.",
    "type": "function",
    "filePath": "services/api/src/modules/media/media.service.js",
    "badges": [
        "public"
    ]
}

Datos y campos

La ruta POST / consume datos de multipart/form-data. El archivo también usa req.auth.userId como contexto de seguridad y trazabilidad.

[
    {
        "name": "file",
        "type": "File",
        "required": true,
        "description": "Archivo recibido por `multer` mediante `upload.single(\"file\")`."
    },
    {
        "name": "listingId",
        "type": "string",
        "description": "Identificador opcional del anuncio. Si existe, la ruta valida que pertenezca al usuario autenticado."
    },
    {
        "name": "folder",
        "type": "string",
        "description": "Carpeta solicitada para ImageKit. Si no llega, usa `listings`."
    },
    {
        "name": "tags",
        "type": "string",
        "description": "Etiquetas separadas por comas. La ruta las recorta, filtra vacías y las convierte en un arreglo."
    }
]
  • req.auth.userId [badge:read-only] string — Identificador del usuario autenticado que se pasa al servicio de subida.

La ruta toma req.body.tags, lo convierte a texto y lo separa por comas. Después aplica trim() a cada valor y elimina los vacíos con filter(Boolean). El resultado se envía como un arreglo al servicio de media.

Comportamientos

Cada handler tiene una responsabilidad concreta. La ruta de autenticación prepara el cliente para subidas futuras desde el navegador. La ruta de carga protege el anuncio y delega la subida al servicio de media.

  • GET /imagekit-auth — genera token, expire y signature, y devuelve la configuración pública de ImageKit.
  • POST / — valida ownership cuando listingId existe y sube el archivo con uploadAssetToImageKit.
  • upload.single("file") — extrae un único archivo de multipart/form-data y lo deja en req.file.
  • console.error("POST /api/uploads error:", error) — registra errores inesperados en la subida.
  • res.status(error.statusCode || 500) — conserva el código de error del servicio cuando está disponible.

[!WARNING] multer.memoryStorage() guarda el archivo en memoria. El límite configurado aquí es de 25 MB.

[!IMPORTANT] Si listingId viene en la petición, la ruta exige que el anuncio pertenezca al usuario autenticado.

API

El archivo define dos endpoints HTTP. Cada uno tiene su propio contrato y su propia respuesta.

GET /imagekit-auth

{
    "title": "GET imagekit auth",
    "description": "Genera los par\u00e1metros de autenticaci\u00f3n de ImageKit y devuelve la configuraci\u00f3n p\u00fablica disponible para el cliente.",
    "method": "GET",
    "baseUrl": "router mount point not shown in this file",
    "endpoint": "/imagekit-auth",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Genera la autenticaci\u00f3n y devuelve datos p\u00fablicos de ImageKit.",
            "body": "{\n  \"success\": true,\n  \"data\": {\n    \"token\": \"imagekit_auth_token\",\n    \"expire\": 1730000000,\n    \"signature\": \"imagekit_signature\"\n  }\n}"
        },
        "500": {
            "description": "Error al generar los par\u00e1metros de autenticaci\u00f3n.",
            "body": "{\n  \"success\": false,\n  \"message\": \"No se pudo generar autenticacion de ImageKit.\"\n}"
        }
    }
}

POST /

{
    "title": "POST upload file",
    "description": "Recibe un archivo multipart, valida ownership si llega listingId y delega la subida a uploadAssetToImageKit.",
    "method": "POST",
    "baseUrl": "router mount point not shown in this file",
    "endpoint": "/",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        },
        {
            "key": "Content-Type",
            "value": "multipart/form-data",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "form",
    "requestBody": "",
    "formData": [
        {
            "key": "file",
            "value": "Binary file content",
            "required": true
        },
        {
            "key": "listingId",
            "value": "Listing ObjectId",
            "required": false
        },
        {
            "key": "folder",
            "value": "listings",
            "required": false
        },
        {
            "key": "tags",
            "value": "hero,cover,public",
            "required": false
        }
    ],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Archivo subido con \u00e9xito.",
            "body": "{\n  \"success\": true,\n  \"data\": {\n    \"result\": \"uploadAssetToImageKit response\"\n  }\n}"
        },
        "403": {
            "description": "El usuario no posee el anuncio asociado.",
            "body": "{\n  \"success\": false,\n  \"message\": \"No puedes subir archivos a este anuncio.\"\n}"
        },
        "500": {
            "description": "Error al procesar la subida.",
            "body": "{\n  \"success\": false,\n  \"message\": \"No se pudo subir el archivo.\"\n}"
        }
    }
}

Casos límite y detalles importantes

Este archivo maneja algunos casos con reglas explícitas. La validación de ownership ocurre solo cuando listingId está presente. Si el valor no es un ObjectId válido, la verificación falla y la ruta responde con 403.

[!NOTE] GET /imagekit-auth está preparado para flujos de subida client-side futuros. El comentario del código indica que nunca expone IMAGEKIT_PRIVATE_KEY.

[!CAUTION] Si listingId no es válido o no pertenece al usuario, la subida se rechaza antes de llamar al servicio de media.

[!WARNING] La ruta de subida no define aquí una validación explícita de req.file. La extracción depende de multer y del servicio delegado.

Relacionado

Estos archivos participan directamente en esta funcionalidad:

  • services/api/src/auth/auth.middleware.js — aporta requireAuth y el contexto req.auth.userId.
  • services/api/src/config/imagekit.js — aporta getImageKitClient() y getImageKitPublicConfig().
  • services/api/src/models/Listing.js — se consulta para verificar que el anuncio pertenece al usuario.
  • services/api/src/modules/media/media.service.js — realiza la subida real con uploadAssetToImageKit().
  • services/api/src/modules/uploads/uploads.routes.js — exporta el router con ambas rutas de uploads.