823 lines
23 KiB
JavaScript
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
|
|
};
|