const express = require('express'); const passport = require('passport'); const bodyParser = require('body-parser'); const services = require('../services/file/'); const { isValidPath, createErrorResponse } = require('../services/file'); const { logger } = require('../utils/logger'); const router = express.Router(); // JSON body parser that ONLY parses application/json content-type // This prevents errors when binary data is sent or no body is present const jsonParser = bodyParser.json({ limit: '1mb', type: (req) => { const contentType = req.headers['content-type'] || ''; return contentType.includes('application/json'); }, }); router.get('/download', (req, res) => { services.downloadFile(req, res); }); // POST /api/file/presign - Generate presigned URLs for multiple assets router.post('/presign', jsonParser, async (req, res) => { const log = req.log || logger; const { urls } = req.body || {}; if (!Array.isArray(urls) || urls.length === 0) { return res .status(400) .json(createErrorResponse('urls array required', 'MISSING_URLS')); } if (urls.length > 50) { return res .status(400) .json( createErrorResponse('Maximum 50 URLs per request', 'TOO_MANY_URLS'), ); } // Validate that all URLs are non-empty strings const invalidUrls = urls.filter( (url) => typeof url !== 'string' || !url.trim(), ); if (invalidUrls.length > 0) { return res .status(400) .json( createErrorResponse( 'All URLs must be non-empty strings', 'INVALID_URL_FORMAT', ), ); } // Validate paths for security (no traversal, no protocols) const unsafeUrls = urls.filter((url) => !isValidPath(url)); if (unsafeUrls.length > 0) { log.warn({ unsafeUrls }, 'Presign request with invalid paths rejected'); return res.status(400).json( createErrorResponse('Invalid file paths detected', 'INVALID_PATH', { invalidPaths: unsafeUrls, }), ); } try { const presignedUrls = await services.generatePresignedUrls(urls); res.json({ presignedUrls }); } catch (error) { log.error( { err: error, urlCount: urls.length }, 'Failed to generate presigned URLs', ); res .status(500) .json( createErrorResponse( 'Failed to generate presigned URLs', 'PRESIGN_ERROR', ), ); } }); router.post( '/upload/:table/:field', passport.authenticate('jwt', { session: false }), (req, res) => { const fileName = `${req.params.table}/${req.params.field}`; services.uploadFile(fileName, req, res); }, ); router.post( '/upload-sessions/init', passport.authenticate('jwt', { session: false }), jsonParser, (req, res) => { services.initUploadSession(req, res); }, ); router.get( '/upload-sessions/:sessionId', passport.authenticate('jwt', { session: false }), (req, res) => { services.getUploadSession(req, res); }, ); // Chunk upload - NO body parser, raw stream is read directly by uploadChunk router.put( '/upload-sessions/:sessionId/chunks/:chunkIndex', passport.authenticate('jwt', { session: false }), (req, res) => { services.uploadChunk(req, res); }, ); // Finalize - NO body parser needed, only uses req.params.sessionId router.post( '/upload-sessions/:sessionId/finalize', passport.authenticate('jwt', { session: false }), (req, res) => { services.finalizeUploadSession(req, res); }, ); module.exports = router;