Fix passenger startup and clean tracked generated files
This commit is contained in:
+20
-8
@@ -1,6 +1,17 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const authenticateDevice = (req, res, next) => {
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const cetDateTimeFormatter = new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: 'Europe/Madrid',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
const authenticateDevice = (req, res, next) => {
|
||||
// 1. Obtener Token del Header 'Authorization: Bearer <token>'
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1]; // Ignorar 'Bearer '
|
||||
@@ -18,10 +29,11 @@ const authenticateDevice = (req, res, next) => {
|
||||
req.user = decoded;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
console.warn(`[Auth] Token inválido desde IP: ${req.ip}`);
|
||||
return res.status(403).json({ success: false, error: 'INVALID_TOKEN', message: 'Token inválido o expirado' });
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const cetDateTime = cetDateTimeFormatter.format(new Date());
|
||||
console.warn(`[Auth] [${cetDateTime} CET] Token inválido desde IP: ${req.ip}`);
|
||||
return res.status(403).json({ success: false, error: 'INVALID_TOKEN', message: 'Token inválido o expirado' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = authenticateDevice;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
|
||||
const MAX_DRIVER_LICENSE_SIZE_BYTES = 5 * 1024 * 1024;
|
||||
const FRONT_FILE_FIELD = 'carnet_conducir_frontal';
|
||||
const BACK_FILE_FIELD = 'carnet_conducir_trasera';
|
||||
|
||||
const MIME_TO_ALLOWED_EXTENSIONS = {
|
||||
'image/jpeg': new Set(['.jpg', '.jpeg']),
|
||||
'image/png': new Set(['.png']),
|
||||
'image/webp': new Set(['.webp'])
|
||||
};
|
||||
|
||||
const ALLOWED_MIME_TYPES = new Set(Object.keys(MIME_TO_ALLOWED_EXTENSIONS));
|
||||
|
||||
const hasAllowedExtensionForMime = (file) => {
|
||||
const extension = path.extname(String(file?.originalname || '')).toLowerCase();
|
||||
const allowedExtensions = MIME_TO_ALLOWED_EXTENSIONS[file.mimetype];
|
||||
|
||||
if (!allowedExtensions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return allowedExtensions.has(extension);
|
||||
};
|
||||
|
||||
const internalUpload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: MAX_DRIVER_LICENSE_SIZE_BYTES,
|
||||
files: 1
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (!ALLOWED_MIME_TYPES.has(file.mimetype) || !hasAllowedExtensionForMime(file)) {
|
||||
return cb(new Error('INVALID_FILE_TYPE'));
|
||||
}
|
||||
|
||||
cb(null, true);
|
||||
}
|
||||
});
|
||||
|
||||
const uploadDriverLicense = (req, res, next) => {
|
||||
internalUpload.fields([
|
||||
{ name: FRONT_FILE_FIELD, maxCount: 1 },
|
||||
{ name: BACK_FILE_FIELD, maxCount: 1 }
|
||||
])(req, res, (error) => {
|
||||
if (!error) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (error instanceof multer.MulterError) {
|
||||
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Archivo demasiado grande. Maximo 5MB.'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === 'LIMIT_FILE_COUNT' || error.code === 'LIMIT_UNEXPECTED_FILE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Solo se permite 1 archivo por request.'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Payload de archivo invalido.'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message === 'INVALID_FILE_TYPE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tipo de archivo invalido. Solo image/jpeg, image/png o image/webp.'
|
||||
});
|
||||
}
|
||||
|
||||
console.error('Driver license upload middleware error:', {
|
||||
message: error.message,
|
||||
code: error.code
|
||||
});
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
FRONT_FILE_FIELD,
|
||||
BACK_FILE_FIELD,
|
||||
uploadDriverLicense,
|
||||
MAX_DRIVER_LICENSE_SIZE_BYTES,
|
||||
ALLOWED_MIME_TYPES
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
|
||||
const MAX_PROFILE_PHOTO_SIZE_BYTES = 5 * 1024 * 1024;
|
||||
const PROFILE_UPLOADS_DIR = path.resolve(__dirname, '..', '..', 'uploads', 'profile');
|
||||
const ALLOWED_MIME_TYPES = new Set(['image/jpeg', 'image/png']);
|
||||
|
||||
const ensureProfileUploadsDir = () => {
|
||||
fs.mkdirSync(PROFILE_UPLOADS_DIR, { recursive: true });
|
||||
};
|
||||
|
||||
const sanitizeFileBaseName = (originalName) => {
|
||||
const baseName = path.basename(originalName || 'profile_photo', path.extname(originalName || ''));
|
||||
const sanitized = baseName
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
.slice(0, 40);
|
||||
|
||||
return sanitized || 'profile_photo';
|
||||
};
|
||||
|
||||
const getExtensionFromMimeType = (mimeType) => (mimeType === 'image/png' ? '.png' : '.jpg');
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
try {
|
||||
ensureProfileUploadsDir();
|
||||
cb(null, PROFILE_UPLOADS_DIR);
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const safeBaseName = sanitizeFileBaseName(file.originalname);
|
||||
const uniqueSuffix = `${Date.now()}_${crypto.randomBytes(6).toString('hex')}`;
|
||||
const extension = getExtensionFromMimeType(file.mimetype);
|
||||
cb(null, `${safeBaseName}_${uniqueSuffix}${extension}`);
|
||||
}
|
||||
});
|
||||
|
||||
const internalUpload = multer({
|
||||
storage,
|
||||
limits: {
|
||||
fileSize: MAX_PROFILE_PHOTO_SIZE_BYTES,
|
||||
files: 1
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (!ALLOWED_MIME_TYPES.has(file.mimetype)) {
|
||||
return cb(new Error('INVALID_FILE_TYPE'));
|
||||
}
|
||||
|
||||
cb(null, true);
|
||||
}
|
||||
});
|
||||
|
||||
const uploadProfilePhoto = (req, res, next) => {
|
||||
internalUpload.single('foto_perfil')(req, res, (error) => {
|
||||
if (!error) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (error instanceof multer.MulterError) {
|
||||
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({ error: 'Archivo demasiado grande. Maximo 5MB.' });
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: 'Archivo invalido.' });
|
||||
}
|
||||
|
||||
if (error.message === 'INVALID_FILE_TYPE') {
|
||||
return res.status(400).json({ error: 'Tipo de archivo invalido. Solo se permite image/jpeg o image/png.' });
|
||||
}
|
||||
|
||||
console.error('Error uploading profile photo:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uploadProfilePhoto
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const requireBearerAuth = (req, res, next) => {
|
||||
const authHeader = req.headers.authorization || '';
|
||||
const [scheme, token] = authHeader.split(' ');
|
||||
|
||||
if (scheme !== 'Bearer' || !token) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
||||
return next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = requireBearerAuth;
|
||||
@@ -0,0 +1,153 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const {
|
||||
getTripStatusUploadsDir,
|
||||
replicateUploadedFilesToRemote,
|
||||
removeUploadedTripStatusFiles
|
||||
} = require('../services/tripStatusPhotoStorage');
|
||||
const { appendPostLog } = require('../utils/postLog');
|
||||
|
||||
const MAX_TRIP_STATUS_PHOTO_SIZE_BYTES = 15 * 1024 * 1024;
|
||||
const MAX_TRIP_STATUS_FILES = 5;
|
||||
const ALLOWED_MIME_TYPES = new Set([
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/heic',
|
||||
'image/heif'
|
||||
]);
|
||||
|
||||
const getTripDirectorySegment = (req) => {
|
||||
const tripId = Number.parseInt(req.params?.id, 10);
|
||||
return Number.isInteger(tripId) && tripId > 0 ? String(tripId) : 'unknown';
|
||||
};
|
||||
|
||||
const getTripStatusUploadsTripDir = (req) =>
|
||||
path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req));
|
||||
|
||||
const ensureTripStatusUploadsDir = (req) => {
|
||||
fs.mkdirSync(getTripStatusUploadsTripDir(req), { recursive: true });
|
||||
};
|
||||
|
||||
const getExtensionFromMimeType = (mimeType) => {
|
||||
if (mimeType === 'image/png') {
|
||||
return '.png';
|
||||
}
|
||||
|
||||
if (mimeType === 'image/webp') {
|
||||
return '.webp';
|
||||
}
|
||||
|
||||
if (mimeType === 'image/heic') {
|
||||
return '.heic';
|
||||
}
|
||||
|
||||
if (mimeType === 'image/heif') {
|
||||
return '.heif';
|
||||
}
|
||||
|
||||
return '.jpg';
|
||||
};
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
try {
|
||||
ensureTripStatusUploadsDir(req);
|
||||
cb(null, getTripStatusUploadsTripDir(req));
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const token = crypto.randomBytes(3).toString('hex');
|
||||
const extension = getExtensionFromMimeType(file.mimetype);
|
||||
cb(null, `${token}${extension}`);
|
||||
}
|
||||
});
|
||||
|
||||
const internalUpload = multer({
|
||||
storage,
|
||||
limits: {
|
||||
fileSize: MAX_TRIP_STATUS_PHOTO_SIZE_BYTES,
|
||||
files: MAX_TRIP_STATUS_FILES
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (!ALLOWED_MIME_TYPES.has(file.mimetype)) {
|
||||
return cb(new Error('INVALID_FILE_TYPE'));
|
||||
}
|
||||
|
||||
cb(null, true);
|
||||
}
|
||||
});
|
||||
|
||||
const uploadTripStatusPhotos = (req, res, next) => {
|
||||
internalUpload.fields([
|
||||
{ name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES },
|
||||
{ name: 'fotos[]', maxCount: MAX_TRIP_STATUS_FILES }
|
||||
])(req, res, async (error) => {
|
||||
if (!error) {
|
||||
const uploadedFiles = collectUploadedTripStatusFiles(req);
|
||||
const authorizationHeader = req.get('authorization');
|
||||
|
||||
appendPostLog({
|
||||
request_id: req.requestId || null,
|
||||
method: req.method,
|
||||
path: req.originalUrl || req.url,
|
||||
ip: req.ip || null,
|
||||
content_type: req.get('content-type') || null,
|
||||
has_authorization_header: Boolean(authorizationHeader),
|
||||
authorization: authorizationHeader ? '[REDACTED]' : null,
|
||||
query: req.query || {},
|
||||
body: req.body || {},
|
||||
raw_body: null,
|
||||
files: uploadedFiles.map((file) => ({
|
||||
field_name: file.fieldname || null,
|
||||
filename: file.filename || null,
|
||||
originalname: file.originalname || null,
|
||||
mimetype: file.mimetype || null,
|
||||
size: Number.isFinite(file.size) ? file.size : null
|
||||
}))
|
||||
});
|
||||
await replicateUploadedFilesToRemote({
|
||||
tripId: req.params?.id,
|
||||
files: uploadedFiles
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (error instanceof multer.MulterError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid payload'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message === 'INVALID_FILE_TYPE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid payload'
|
||||
});
|
||||
}
|
||||
|
||||
console.error('Error uploading trip status photos:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const collectUploadedTripStatusFiles = (req) => [
|
||||
...(Array.isArray(req.files?.fotos) ? req.files.fotos : []),
|
||||
...(Array.isArray(req.files?.['fotos[]']) ? req.files['fotos[]'] : [])
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
uploadTripStatusPhotos,
|
||||
collectUploadedTripStatusFiles,
|
||||
removeUploadedTripStatusFiles,
|
||||
MAX_TRIP_STATUS_FILES
|
||||
};
|
||||
Reference in New Issue
Block a user