ABIANAPP_NODE_PRODUCCION/src/controllers/driverLicenseController.js

823 lines
23 KiB
JavaScript

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
};