const crypto = require('crypto'); const db = require('../config/db'); const { FRONT_FILE_FIELD, BACK_FILE_FIELD } = require('../middleware/driverLicenseUpload'); const { encryptBuffer } = require('../services/driverLicenseCrypto'); const { persistEncryptedBuffer, readDecryptedBuffer, removeStoredFile } = require('../services/driverLicenseStorage'); const DRIVER_LICENSE_DOCUMENT_TYPE = 'driver_license'; const DEFAULT_RETENTION_DAYS = 365; const ADMIN_ROLES = new Set(['admin', 'superadmin', 'compliance', 'backoffice']); const DOCUMENT_SIDES = new Set(['front', 'back']); const MIME_EXTENSIONS = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp' }; const SIDE_TO_FILE_FIELD = { front: FRONT_FILE_FIELD, back: BACK_FILE_FIELD }; const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const normalizeDni = (rawDni) => String(rawDni || '').trim().toUpperCase(); const parseProviderId = (rawProviderId) => { const parsed = Number.parseInt(String(rawProviderId || '').trim(), 10); if (!Number.isInteger(parsed) || parsed <= 0) { return null; } return parsed; }; const parseDocumentSide = (rawSide) => { const side = String(rawSide || '').trim().toLowerCase(); return DOCUMENT_SIDES.has(side) ? side : null; }; const parseRetentionDays = () => { const parsed = Number.parseInt(String(process.env.DRIVER_LICENSE_RETENTION_DAYS || ''), 10); if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 3650) { return DEFAULT_RETENTION_DAYS; } return parsed; }; const getDriverLicenseKeyVersion = () => { const keyVersion = String( process.env.DRIVER_LICENSE_KEY_VERSION || process.env.DRIVER_LICENSE_ENCRYPTION_KEY_VERSION || 'v1' ) .trim() .slice(0, 32); return keyVersion || 'v1'; }; const isPrivilegedUser = (user) => { const role = String(user?.role || '').trim().toLowerCase(); const isAdminFlag = user?.is_admin === true || user?.is_admin === 1 || user?.is_admin === '1'; return isAdminFlag || ADMIN_ROLES.has(role); }; const isAuthorizedForTarget = (user, dni, idProveedor) => { if (!user) { return false; } if (isPrivilegedUser(user)) { return true; } const userDni = normalizeDni(user.dni); const userProviderId = parseProviderId(user.id_proveedor); return userDni === dni && userProviderId === idProveedor; }; const getClientIp = (req) => String(req.ip || '').slice(0, 45); const getUserAgent = (req) => String(req.headers['user-agent'] || '').slice(0, 255); const parseUploaderId = (user) => { const parsed = Number.parseInt(String(user?.id || ''), 10); return Number.isInteger(parsed) && parsed > 0 ? parsed : null; }; const detectMimeTypeFromSignature = (buffer) => { if (!Buffer.isBuffer(buffer) || buffer.length < 12) { return null; } if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { return 'image/jpeg'; } const isPng = buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47 && buffer[4] === 0x0d && buffer[5] === 0x0a && buffer[6] === 0x1a && buffer[7] === 0x0a; if (isPng) { return 'image/png'; } const riff = buffer.subarray(0, 4).toString('ascii'); const webp = buffer.subarray(8, 12).toString('ascii'); if (riff === 'RIFF' && webp === 'WEBP') { return 'image/webp'; } return null; }; const buildPublicReference = (side, publicId) => `secure/driver-license/${side}/${publicId}`; const buildApiResponse = (side, publicId) => { const reference = buildPublicReference(side, publicId); const baseResponse = { success: true, document_side: side, data: { document_side: side } }; if (side === 'front') { baseResponse.carnet_conducir_frontal = reference; baseResponse.driverLicenseFrontImage = reference; baseResponse.carnet_conducir = reference; baseResponse.data.carnet_conducir_frontal = reference; } else { baseResponse.carnet_conducir_trasera = reference; baseResponse.driverLicenseBackImage = reference; baseResponse.carnet_conducir = reference; baseResponse.data.carnet_conducir_trasera = reference; } return baseResponse; }; const getUploadedFileForSide = (req, side) => { const frontFiles = Array.isArray(req.files?.[FRONT_FILE_FIELD]) ? req.files[FRONT_FILE_FIELD] : []; const backFiles = Array.isArray(req.files?.[BACK_FILE_FIELD]) ? req.files[BACK_FILE_FIELD] : []; const totalFiles = frontFiles.length + backFiles.length; if (totalFiles === 0) { return { ok: false, error: `Archivo requerido. Para ${side} usa el campo ${SIDE_TO_FILE_FIELD[side]}.` }; } if (totalFiles > 1) { return { ok: false, error: 'Solo se permite 1 archivo por request.' }; } if (side === 'front' && frontFiles.length === 1) { return { ok: true, file: frontFiles[0] }; } if (side === 'back' && backFiles.length === 1) { return { ok: true, file: backFiles[0] }; } return { ok: false, error: `Para document_side=${side} debe enviarse ${SIDE_TO_FILE_FIELD[side]}.` }; }; const recordAuditEvent = async ({ fileId, action, actorUserId, dni, idProveedor, side, ip, userAgent }) => { try { await db.query( `INSERT INTO driver_license_access_audit (driver_license_file_id, action, actor_user_id, dni_target, id_proveedor_target, side_target, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ fileId || null, action, actorUserId || null, dni || null, idProveedor || null, side || null, ip || null, userAgent || null ] ); } catch (auditError) { console.error('Driver license audit insert failed:', { message: auditError.message, action, fileId: fileId || null, actorUserId: actorUserId || null, dni: dni || null, idProveedor: idProveedor || null, side: side || null }); } }; const ensureTargetExists = async (dni, idProveedor) => { const [rows] = await db.query( `SELECT id_transportista FROM m_proveedores_trasportistas WHERE dni = ? AND id_proveedor = ? AND desactivado = 0 LIMIT 1`, [dni, idProveedor] ); return rows.length > 0; }; const findDriverLicenseFileByPublicId = async (publicId) => { const [rows] = await db.query( `SELECT id, public_id, dni, id_proveedor, side, storage_key, mime_type, encryption_alg, encryption_iv, encryption_tag, expires_at, deleted_at FROM driver_license_files WHERE public_id = ? LIMIT 1`, [publicId] ); return rows[0] || null; }; const findActiveDriverLicenseByTargetAndSide = async (dni, idProveedor, side) => { const [rows] = await db.query( `SELECT id, public_id, dni, id_proveedor, side, storage_key, mime_type, encryption_alg, encryption_iv, encryption_tag, expires_at, deleted_at FROM driver_license_files WHERE dni = ? AND id_proveedor = ? AND side = ? AND document_type = ? AND deleted_at IS NULL ORDER BY id DESC LIMIT 1`, [dni, idProveedor, side, DRIVER_LICENSE_DOCUMENT_TYPE] ); return rows[0] || null; }; const hasExpired = (expiresAt) => { if (!expiresAt) { return false; } return new Date(expiresAt).getTime() <= Date.now(); }; const markAsDeleted = async (fileId) => { await db.query( `UPDATE driver_license_files SET deleted_at = COALESCE(deleted_at, NOW()) WHERE id = ?`, [fileId] ); }; const validateUploadPayload = (req) => { const dni = normalizeDni(req.body?.dni); const idProveedor = parseProviderId(req.body?.id_proveedor); const documentType = String(req.body?.document_type || '').trim(); const side = parseDocumentSide(req.body?.document_side); if (!dni || !idProveedor || documentType !== DRIVER_LICENSE_DOCUMENT_TYPE) { return { ok: false, statusCode: 400, body: { success: false, error: 'dni, id_proveedor y document_type=driver_license son obligatorios.' } }; } if (!side) { return { ok: false, statusCode: 400, body: { success: false, error: 'document_side invalido. Valores permitidos: front, back.' } }; } const fileSelection = getUploadedFileForSide(req, side); if (!fileSelection.ok) { return { ok: false, statusCode: 400, body: { success: false, error: fileSelection.error } }; } const detectedMime = detectMimeTypeFromSignature(fileSelection.file.buffer); if (!detectedMime || detectedMime !== fileSelection.file.mimetype) { return { ok: false, statusCode: 400, body: { success: false, error: 'Archivo invalido: el contenido no coincide con el tipo permitido.' } }; } return { ok: true, dni, idProveedor, side, file: fileSelection.file, detectedMime }; }; const updateDriverLicense = async (req, res) => { const uploadValidation = validateUploadPayload(req); if (!uploadValidation.ok) { return res.status(uploadValidation.statusCode).json(uploadValidation.body); } const { dni, idProveedor, side, file, detectedMime } = uploadValidation; const uploaderId = parseUploaderId(req.user); if (!isAuthorizedForTarget(req.user, dni, idProveedor)) { await recordAuditEvent({ fileId: null, action: 'upload_forbidden', actorUserId: uploaderId, dni, idProveedor, side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(403).json({ success: false, error: 'Forbidden' }); } let storageKey = null; let connection; let insertedFileId = null; try { const targetExists = await ensureTargetExists(dni, idProveedor); if (!targetExists) { await recordAuditEvent({ fileId: null, action: 'upload_forbidden', actorUserId: uploaderId, dni, idProveedor, side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(403).json({ success: false, error: 'Forbidden' }); } const publicId = crypto.randomUUID(); const checksumSha256 = crypto .createHash('sha256') .update(file.buffer) .digest('hex'); const retentionDays = parseRetentionDays(); const expiresAt = new Date(Date.now() + retentionDays * 24 * 60 * 60 * 1000); const encrypted = encryptBuffer(file.buffer); const persisted = await persistEncryptedBuffer(encrypted.ciphertext); storageKey = persisted.storageKey; connection = await db.getConnection(); await connection.beginTransaction(); await connection.query( `UPDATE driver_license_files SET deleted_at = NOW() WHERE dni = ? AND id_proveedor = ? AND side = ? AND document_type = ? AND deleted_at IS NULL`, [dni, idProveedor, side, DRIVER_LICENSE_DOCUMENT_TYPE] ); const [insertResult] = await connection.query( `INSERT INTO driver_license_files (public_id, dni, id_proveedor, side, document_type, storage_key, mime_type, size_bytes, checksum_sha256, encryption_alg, encryption_iv, encryption_tag, key_version, uploaded_by, expires_at, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`, [ publicId, dni, idProveedor, side, DRIVER_LICENSE_DOCUMENT_TYPE, storageKey, detectedMime, file.size, checksumSha256, encrypted.algorithm, encrypted.ivHex, encrypted.authTagHex, getDriverLicenseKeyVersion(), uploaderId, expiresAt ] ); insertedFileId = insertResult.insertId; await connection.commit(); await recordAuditEvent({ fileId: insertedFileId, action: 'upload_success', actorUserId: uploaderId, dni, idProveedor, side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(200).json(buildApiResponse(side, publicId)); } catch (error) { if (connection) { try { await connection.rollback(); } catch (rollbackError) { console.error('Rollback failed after driver license upload error:', { message: rollbackError.message, dni, idProveedor, side }); } } if (storageKey) { try { await removeStoredFile(storageKey); } catch (deleteError) { console.error('Failed to cleanup encrypted driver license after error:', { message: deleteError.message, dni, idProveedor, side }); } } console.error('Error uploading driver license:', { message: error.message, dni, idProveedor, side, userId: uploaderId }); return res.status(500).json({ success: false, error: 'Internal server error' }); } finally { if (connection) { connection.release(); } } }; const sendFileRecord = async (res, fileRecord) => { const decrypted = await readDecryptedBuffer(fileRecord.storage_key, { algorithm: fileRecord.encryption_alg, ivHex: fileRecord.encryption_iv, authTagHex: fileRecord.encryption_tag }); const extension = MIME_EXTENSIONS[fileRecord.mime_type] || '.bin'; res.setHeader('Content-Type', fileRecord.mime_type || 'application/octet-stream'); res.setHeader('Content-Length', String(decrypted.length)); res.setHeader('Cache-Control', 'private, no-store, max-age=0'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader( 'Content-Disposition', `inline; filename="driver_license_${fileRecord.side}_${fileRecord.public_id}${extension}"` ); return res.status(200).send(decrypted); }; const sendDriverLicenseBySide = async (req, res) => { const side = parseDocumentSide(req.params?.side); const dni = normalizeDni(req.query?.dni || req.user?.dni); const idProveedor = parseProviderId(req.query?.id_proveedor || req.user?.id_proveedor); const actorUserId = parseUploaderId(req.user); if (!side) { return res.status(400).json({ success: false, error: 'document_side invalido. Valores permitidos: front, back.' }); } if (!dni || !idProveedor) { return res.status(400).json({ success: false, error: 'dni e id_proveedor son obligatorios.' }); } if (!isAuthorizedForTarget(req.user, dni, idProveedor)) { await recordAuditEvent({ fileId: null, action: 'download_forbidden', actorUserId, dni, idProveedor, side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(403).json({ success: false, error: 'Forbidden' }); } try { const fileRecord = await findActiveDriverLicenseByTargetAndSide(dni, idProveedor, side); if (!fileRecord || fileRecord.deleted_at) { return res.status(404).json({ success: false, error: 'Not found' }); } if (hasExpired(fileRecord.expires_at)) { await markAsDeleted(fileRecord.id); await recordAuditEvent({ fileId: fileRecord.id, action: 'download_expired', actorUserId, dni, idProveedor, side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(404).json({ success: false, error: 'Not found' }); } await recordAuditEvent({ fileId: fileRecord.id, action: 'download_success', actorUserId, dni, idProveedor, side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return sendFileRecord(res, fileRecord); } catch (error) { console.error('Error retrieving driver license by side:', { message: error.message, dni, idProveedor, side, userId: actorUserId }); return res.status(500).json({ success: false, error: 'Internal server error' }); } }; const sendDriverLicense = async (req, res) => { const publicId = String(req.params?.publicId || '').trim(); const actorUserId = parseUploaderId(req.user); if (!UUID_V4_REGEX.test(publicId)) { return res.status(400).json({ success: false, error: 'Invalid id' }); } try { const fileRecord = await findDriverLicenseFileByPublicId(publicId); if (!fileRecord || fileRecord.deleted_at) { return res.status(404).json({ success: false, error: 'Not found' }); } if (hasExpired(fileRecord.expires_at)) { await markAsDeleted(fileRecord.id); await recordAuditEvent({ fileId: fileRecord.id, action: 'download_expired', actorUserId, dni: fileRecord.dni, idProveedor: fileRecord.id_proveedor, side: fileRecord.side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(404).json({ success: false, error: 'Not found' }); } if (!isAuthorizedForTarget(req.user, normalizeDni(fileRecord.dni), parseProviderId(fileRecord.id_proveedor))) { await recordAuditEvent({ fileId: fileRecord.id, action: 'download_forbidden', actorUserId, dni: fileRecord.dni, idProveedor: fileRecord.id_proveedor, side: fileRecord.side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(403).json({ success: false, error: 'Forbidden' }); } await recordAuditEvent({ fileId: fileRecord.id, action: 'download_success', actorUserId, dni: fileRecord.dni, idProveedor: fileRecord.id_proveedor, side: fileRecord.side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return sendFileRecord(res, fileRecord); } catch (error) { console.error('Error retrieving driver license by id:', { message: error.message, publicId, userId: actorUserId }); return res.status(500).json({ success: false, error: 'Internal server error' }); } }; const deleteDriverLicense = async (req, res) => { const publicId = String(req.params?.publicId || '').trim(); const actorUserId = parseUploaderId(req.user); if (!UUID_V4_REGEX.test(publicId)) { return res.status(400).json({ success: false, error: 'Invalid id' }); } try { const fileRecord = await findDriverLicenseFileByPublicId(publicId); if (!fileRecord || fileRecord.deleted_at) { return res.status(404).json({ success: false, error: 'Not found' }); } if (!isAuthorizedForTarget(req.user, normalizeDni(fileRecord.dni), parseProviderId(fileRecord.id_proveedor))) { await recordAuditEvent({ fileId: fileRecord.id, action: 'delete_forbidden', actorUserId, dni: fileRecord.dni, idProveedor: fileRecord.id_proveedor, side: fileRecord.side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(403).json({ success: false, error: 'Forbidden' }); } await markAsDeleted(fileRecord.id); await recordAuditEvent({ fileId: fileRecord.id, action: 'delete_success', actorUserId, dni: fileRecord.dni, idProveedor: fileRecord.id_proveedor, side: fileRecord.side, ip: getClientIp(req), userAgent: getUserAgent(req) }); return res.status(200).json({ success: true }); } catch (error) { console.error('Error deleting driver license:', { message: error.message, publicId, userId: actorUserId }); return res.status(500).json({ success: false, error: 'Internal server error' }); } }; module.exports = { updateDriverLicense, sendDriverLicense, sendDriverLicenseBySide, deleteDriverLicense, DRIVER_LICENSE_DOCUMENT_TYPE, buildPublicReference };