134 lines
3.4 KiB
JavaScript
134 lines
3.4 KiB
JavaScript
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;
|