se agrega los requerimientos de la LODPD con firma de documentos

This commit is contained in:
2026-04-07 15:14:44 -05:00
parent 408a9b4435
commit 46a11728b8
31 changed files with 1432 additions and 52 deletions
+12 -1
View File
@@ -9,7 +9,18 @@ const getContext = (req) => ({
const register = async (req, res, next) => {
try {
const result = await authService.register(req.body);
if (!req.file) {
const error = new Error('Debes cargar un certificado P12 o PFX.');
error.statusCode = 400;
throw error;
}
const result = await authService.register({
email: req.body.email,
password: req.body.password,
certPassword: req.body.certPassword,
certificateAlias: req.body.certificateAlias,
certificateFile: req.file,
}, getContext(req));
return sendSuccess(res, {
message: 'Registration successful. Please check your email to verify your account.',
userId: result.userId,
@@ -98,6 +98,10 @@ const updatePassword = async (userId, passwordHash) => {
);
};
const deleteUser = async (userId) => {
await db.execute('DELETE FROM users WHERE id = ?', [userId]);
};
module.exports = {
findByEmail,
findById,
@@ -112,4 +116,5 @@ module.exports = {
setResetToken,
findByResetToken,
updatePassword,
deleteUser,
};
+3 -2
View File
@@ -1,11 +1,12 @@
const express = require('express');
const authController = require('./auth.controller');
const { validate, registerSchema, loginSchema, refreshSchema, changePasswordSchema } = require('./auth.validator');
const { validate, validateRegisterMultipart, loginSchema, refreshSchema, changePasswordSchema } = require('./auth.validator');
const authenticate = require('../../middlewares/authenticate');
const { uploadTempCertificate } = require('../../config/multer');
const router = express.Router();
router.post('/register', validate(registerSchema), authController.register);
router.post('/register', uploadTempCertificate.single('certificateFile'), validateRegisterMultipart, authController.register);
router.post('/login', validate(loginSchema), authController.login);
router.post('/refresh', validate(refreshSchema), authController.refresh);
router.post('/logout', authController.logout);
+57 -7
View File
@@ -5,7 +5,12 @@ const passwordChangeRepository = require('./password-change.repository');
const auditService = require('../audit/audit.service');
const usersService = require('../users/users.service');
const mailService = require('../mail/mail.service');
const certificatesService = require('../certificates/certificates.service');
const legalService = require('../legal/legal.service');
const { getCertificatePath } = require('../../utils/storagePath');
const { signAccess, signRefresh } = require('../../utils/jwt');
const fs = require('fs/promises');
const path = require('path');
const SALT_ROUNDS = 12;
@@ -37,7 +42,7 @@ const issueVerificationEmail = async (user) => {
};
};
const register = async ({ email, password }) => {
const register = async ({ email, password, certPassword, certificateFile, certificateAlias }, context = {}) => {
const existing = await authRepository.findByEmail(email);
if (existing) {
const err = new Error('Email already registered.');
@@ -57,17 +62,56 @@ const register = async ({ email, password }) => {
verificationToken,
verificationTokenExpires,
});
let mailResult;
let uploadedCertificate = null;
let legalDocuments = [];
try {
mailResult = await mailService.sendVerificationEmail({
const certificateDir = await getCertificatePath(userId);
const certificateFilename = path.basename(certificateFile.path);
const finalCertificatePath = path.join(certificateDir, certificateFilename);
await fs.rename(certificateFile.path, finalCertificatePath);
uploadedCertificate = await certificatesService.upload({
...certificateFile,
path: finalCertificatePath,
filename: certificateFilename,
}, certPassword, userId, certificateAlias || certificateFile.originalname, context);
const p12Buffer = await fs.readFile(finalCertificatePath);
legalDocuments = await legalService.issueInitialLegalPackage({
userId,
userName: name,
userEmail: email,
signerName: uploadedCertificate.subject_cn || name,
p12Buffer,
p12Password: certPassword,
context,
});
const attachments = legalDocuments.map((item) => ({
filename: item.signedFilename,
path: item.signedPath,
}));
await mailService.sendVerificationEmail({
name,
email,
verificationToken,
attachments,
});
} catch (error) {
const err = new Error('The account was created, but the verification email could not be sent. Please contact support or try again later.');
err.statusCode = 503;
err.code = 'MAIL_SEND_FAILED';
await authRepository.deleteUser(userId);
try {
if (certificateFile?.path) {
await fs.unlink(certificateFile.path);
}
} catch {}
const err = new Error('No se pudo completar el registro legal inicial. Revisa el certificado, la contraseña del P12 y vuelve a intentarlo.');
err.statusCode = error.statusCode || 503;
err.code = error.code || 'REGISTER_LEGAL_FLOW_FAILED';
err.message = error.message || err.message;
throw err;
}
@@ -77,10 +121,16 @@ const register = async ({ email, password }) => {
email,
metadata: {
verificationEmailSent: true,
legalPackageCreated: true,
legalVersion: '1.0.1',
},
});
return { userId, verificationToken, verificationUrl: mailResult.verificationUrl };
return {
userId,
verificationToken,
legalDocuments: legalDocuments.map(({ signedPath, originalPath, ...item }) => item),
};
};
const login = async ({ email, password }, context = {}) => {
+48 -5
View File
@@ -17,10 +17,17 @@ const isStrongPassword = (value, helpers) => {
return value;
};
const registerSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().max(100).required().custom(isStrongPassword),
});
const passwordRuleCheckers = [
{ test: (value) => value.length >= 8, label: 'al menos 8 caracteres' },
{ test: (value) => /[A-Z]/.test(value), label: 'una mayúscula' },
{ test: (value) => /[a-z]/.test(value), label: 'una minúscula' },
{ test: (value) => /\d/.test(value), label: 'un número' },
{ test: (value) => /[^A-Za-z0-9]/.test(value), label: 'un carácter especial' },
];
const getPasswordIssues = (value = '') => passwordRuleCheckers
.filter((rule) => !rule.test(value))
.map((rule) => rule.label);
const loginSchema = Joi.object({
email: Joi.string().email().required(),
@@ -36,6 +43,42 @@ const changePasswordSchema = Joi.object({
newPassword: Joi.string().max(100).required().custom(isStrongPassword),
});
const validateRegisterMultipart = (req, res, next) => {
const { email, password, certPassword, consentAccepted } = req.body;
const issues = [];
if (!email || Joi.string().email().validate(email).error) {
issues.push('Correo electrónico inválido');
}
if (!password) {
issues.push('La contraseña es obligatoria');
} else {
const passwordIssues = getPasswordIssues(password);
if (passwordIssues.length > 0) {
issues.push(`La contraseña debe incluir ${passwordIssues.join(', ')}`);
}
}
if (!certPassword) {
issues.push('La contraseña del certificado es obligatoria para firmar los documentos legales');
}
if (!req.file) {
issues.push('Debes cargar un certificado P12 o PFX');
}
if (!['true', true, 'on', '1', 1].includes(consentAccepted)) {
issues.push('Debes aceptar los documentos legales para completar el registro');
}
if (issues.length > 0) {
return res.status(400).json({ success: false, data: null, error: issues.join('; ') });
}
return next();
};
const validate = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
@@ -45,4 +88,4 @@ const validate = (schema) => (req, res, next) => {
next();
};
module.exports = { validate, registerSchema, loginSchema, refreshSchema, changePasswordSchema };
module.exports = { validate, validateRegisterMultipart, loginSchema, refreshSchema, changePasswordSchema };
@@ -0,0 +1,58 @@
const legalService = require('./legal.service');
const { sendSuccess } = require('../../utils/apiResponse');
const list = async (req, res, next) => {
try {
const items = await legalService.listByUser(req.user.id);
return sendSuccess(res, items, 200);
} catch (error) {
return next(error);
}
};
const getOne = async (req, res, next) => {
try {
const item = await legalService.getOne(req.params.id, req.user.id);
const { originalPath, signedPath, ...safe } = item;
return sendSuccess(res, safe, 200);
} catch (error) {
return next(error);
}
};
const download = async (req, res, next) => {
try {
const item = await legalService.getOne(req.params.id, req.user.id);
return res.download(item.signedPath, item.signedFilename);
} catch (error) {
return next(error);
}
};
const templates = async (req, res, next) => {
try {
const items = await legalService.getTemplates();
return sendSuccess(res, items, 200);
} catch (error) {
return next(error);
}
};
const templatePreview = async (req, res, next) => {
try {
const { filename, pdfBytes } = await legalService.getTemplatePreview(req.params.slug);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
return res.send(Buffer.from(pdfBytes));
} catch (error) {
return next(error);
}
};
module.exports = {
list,
getOne,
download,
templates,
templatePreview,
};
@@ -0,0 +1,45 @@
const fs = require('fs/promises');
const path = require('path');
const env = require('../../config/env');
const metadataDir = path.join(env.storagePath, 'legal', 'metadata');
const metadataFile = path.join(metadataDir, 'legal-documents.json');
const readAll = async () => {
try {
const raw = await fs.readFile(metadataFile, 'utf8');
return JSON.parse(raw);
} catch {
return [];
}
};
const writeAll = async (items) => {
await fs.mkdir(metadataDir, { recursive: true });
await fs.writeFile(metadataFile, JSON.stringify(items, null, 2), 'utf8');
};
const createMany = async (items) => {
const current = await readAll();
current.push(...items);
await writeAll(current);
return items;
};
const findByUser = async (userId) => {
const current = await readAll();
return current
.filter((item) => item.userId === userId)
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
};
const findById = async (id) => {
const current = await readAll();
return current.find((item) => item.id === id) || null;
};
module.exports = {
createMany,
findByUser,
findById,
};
+13
View File
@@ -0,0 +1,13 @@
const express = require('express');
const authenticate = require('../../middlewares/authenticate');
const legalController = require('./legal.controller');
const router = express.Router();
router.get('/templates', legalController.templates);
router.get('/templates/:slug/preview', legalController.templatePreview);
router.get('/', authenticate, legalController.list);
router.get('/:id', authenticate, legalController.getOne);
router.get('/:id/download', authenticate, legalController.download);
module.exports = router;
+225
View File
@@ -0,0 +1,225 @@
const fs = require('fs/promises');
const path = require('path');
const crypto = require('crypto');
const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
const legalRepository = require('./legal.repository');
const { getLegalTemplates, LEGAL_VERSION, EFFECTIVE_DATE } = require('./legal.templates');
const { getLegalDocumentPath } = require('../../utils/storagePath');
const { signPdf } = require('../signing/signing.service');
const auditService = require('../audit/audit.service');
const wrapText = (text, font, size, maxWidth) => {
const words = String(text).split(/\s+/);
const lines = [];
let current = '';
words.forEach((word) => {
const candidate = current ? `${current} ${word}` : word;
if (font.widthOfTextAtSize(candidate, size) <= maxWidth) {
current = candidate;
} else {
if (current) lines.push(current);
current = word;
}
});
if (current) lines.push(current);
return lines;
};
const buildLegalPdfBytes = async ({ userName, userEmail, template }) => {
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([595.28, 841.89]);
const bold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const regular = await pdfDoc.embedFont(StandardFonts.Helvetica);
const margin = 46;
const width = page.getWidth() - (margin * 2);
let y = page.getHeight() - 56;
page.drawText('LinkedSign', { x: margin, y, size: 18, font: bold, color: rgb(0.09, 0.40, 0.75) });
y -= 28;
page.drawText(template.title, { x: margin, y, size: 15, font: bold, color: rgb(0.08, 0.10, 0.18) });
y -= 18;
page.drawText(`Versión ${template.version} · Vigente desde ${template.effectiveDate}`, { x: margin, y, size: 10, font: regular, color: rgb(0.38, 0.44, 0.60) });
y -= 20;
page.drawText(`Titular: ${userName}`, { x: margin, y, size: 10, font: regular, color: rgb(0.08, 0.10, 0.18) });
y -= 14;
page.drawText(`Correo: ${userEmail}`, { x: margin, y, size: 10, font: regular, color: rgb(0.08, 0.10, 0.18) });
y -= 18;
const summaryLines = wrapText(template.summary, regular, 10.5, width);
summaryLines.forEach((line) => {
page.drawText(line, { x: margin, y, size: 10.5, font: regular, color: rgb(0.25, 0.29, 0.39) });
y -= 13;
});
y -= 10;
template.sections.forEach((section) => {
page.drawText(section.heading, { x: margin, y, size: 11.5, font: bold, color: rgb(0.08, 0.10, 0.18) });
y -= 15;
section.body.forEach((paragraph) => {
const lines = wrapText(paragraph, regular, 10, width - 12);
lines.forEach((line, index) => {
const text = index === 0 ? `• ${line}` : ` ${line}`;
page.drawText(text, { x: margin, y, size: 10, font: regular, color: rgb(0.22, 0.25, 0.34) });
y -= 12;
});
y -= 6;
});
y -= 4;
});
return pdfDoc.save();
};
const buildLegalPdf = async ({ userName, userEmail, template, outputPath }) => {
const pdfBytes = await buildLegalPdfBytes({ userName, userEmail, template });
await fs.writeFile(outputPath, pdfBytes);
return pdfBytes;
};
const sanitizeSlug = (value) => value.replace(/[^a-z0-9-]/gi, '_');
const issueInitialLegalPackage = async ({
userId,
userName,
userEmail,
signerName,
p12Buffer,
p12Password,
context = {},
}) => {
const originalDir = await getLegalDocumentPath(userId, 'original');
const signedDir = await getLegalDocumentPath(userId, 'signed');
const timestamp = new Date();
const stamp = [
timestamp.getFullYear(),
String(timestamp.getMonth() + 1).padStart(2, '0'),
String(timestamp.getDate()).padStart(2, '0'),
String(timestamp.getHours()).padStart(2, '0'),
String(timestamp.getMinutes()).padStart(2, '0'),
].join('');
const templates = getLegalTemplates();
const created = [];
for (const template of templates) {
const baseName = `${sanitizeSlug(template.slug)}_${stamp}`;
const originalFilename = `${baseName}.pdf`;
const originalPath = path.join(originalDir, originalFilename);
await buildLegalPdf({
userName,
userEmail,
template,
outputPath: originalPath,
});
const { signedFilename, signedPath } = await signPdf({
pdfPath: originalPath,
p12Buffer,
p12Password,
signerName,
reason: 'Aceptación legal de registro',
location: 'LinkedSign',
signatureStyle: 'qr',
qrPosition: 'right',
qrBackground: 'transparent',
qrBorder: 'none',
outputDir: signedDir,
outputBaseName: `${baseName}_signed`,
});
const signedBuffer = await fs.readFile(signedPath);
const hashSha256 = crypto.createHash('sha256').update(signedBuffer).digest('hex');
created.push({
id: crypto.randomUUID(),
userId,
slug: template.slug,
title: template.title,
version: template.version,
effectiveDate: template.effectiveDate,
createdAt: timestamp.toISOString(),
originalFilename,
originalPath,
signedFilename,
signedPath,
hashSha256,
});
}
await legalRepository.createMany(created);
await auditService.logEvent({
type: 'LEGAL_PACKAGE_SIGNED',
userId,
email: userEmail,
ip: context.ip,
userAgent: context.userAgent,
metadata: {
version: LEGAL_VERSION,
documents: created.map((item) => ({ slug: item.slug, title: item.title })),
},
});
return created;
};
const listByUser = async (userId) => {
const items = await legalRepository.findByUser(userId);
return items.map(({ originalPath, signedPath, ...safe }) => safe);
};
const getOne = async (id, userId) => {
const item = await legalRepository.findById(id);
if (!item) {
const error = new Error('Legal document not found.');
error.statusCode = 404;
throw error;
}
if (item.userId !== userId) {
const error = new Error('Access denied.');
error.statusCode = 403;
throw error;
}
return item;
};
const getTemplates = async () => getLegalTemplates().map((template) => ({
slug: template.slug,
title: template.title,
version: template.version,
effectiveDate: template.effectiveDate,
summary: template.summary,
sections: template.sections,
}));
const getTemplatePreview = async (slug) => {
const template = getLegalTemplates().find((item) => item.slug === slug);
if (!template) {
const error = new Error('Legal template not found.');
error.statusCode = 404;
throw error;
}
const pdfBytes = await buildLegalPdfBytes({
userName: 'Vista previa de registro',
userEmail: 'preview@linkedsign.local',
template,
});
return {
filename: `${sanitizeSlug(slug)}_preview.pdf`,
pdfBytes,
};
};
module.exports = {
issueInitialLegalPackage,
listByUser,
getOne,
getTemplates,
getTemplatePreview,
};
@@ -0,0 +1,170 @@
const env = require('../../config/env');
const LEGAL_VERSION = '1.1.0';
const EFFECTIVE_DATE = '2026-04-07';
const buildControllerBlock = () => {
const {
controllerName,
platformName,
contactEmail,
contactPhone,
contactAddress,
website,
rightsEmail,
} = env.legal;
return {
controllerName,
platformName,
contactEmail,
contactPhone,
contactAddress,
website,
rightsEmail,
};
};
const getLegalTemplates = () => {
const controller = buildControllerBlock();
return [
{
slug: 'terms-of-use',
title: `Términos de Uso de ${controller.platformName}`,
version: LEGAL_VERSION,
effectiveDate: EFFECTIVE_DATE,
summary: `Condiciones de acceso y uso de ${controller.platformName} como plataforma de gestión documental y firma electrónica.`,
sections: [
{
heading: 'Identificación del prestador',
body: [
`${controller.controllerName} opera ${controller.platformName} como plataforma tecnológica para gestión documental, firma electrónica, administración de certificados y conservación de evidencia técnica asociada.`,
`Canales de contacto del responsable: correo ${controller.contactEmail}, teléfono ${controller.contactPhone}, dirección ${controller.contactAddress} y sitio web ${controller.website}.`,
],
},
{
heading: 'Objeto y alcance',
body: [
`Estos términos regulan el acceso y uso de ${controller.platformName}. La persona usuaria declara que utilizará el sistema de forma lícita, veraz, diligente y conforme a la normativa ecuatoriana aplicable.`,
'El servicio está orientado a personas que cuentan con capacidad legal suficiente y, cuando corresponda, con un certificado digital válido y autorizado para su uso.',
],
},
{
heading: 'Responsabilidades de la persona usuaria',
body: [
'La persona usuaria es responsable del resguardo de sus credenciales, de la confidencialidad de su certificado digital y del contenido de los documentos que cargue o firme dentro del sistema.',
'La persona usuaria declara que el archivo P12 o PFX cargado le pertenece o que cuenta con autorización suficiente para usarlo y que su utilización no vulnera derechos de terceros.',
],
},
{
heading: 'Uso de la firma electrónica y de la plataforma',
body: [
`${controller.platformName} procesa documentos PDF, certificados digitales, metadatos de firma y evidencia técnica para habilitar firma electrónica visible y criptográfica, verificación y trazabilidad de operaciones.`,
'La contraseña del certificado proporcionada durante el registro inicial se utiliza exclusivamente para firmar los documentos legales de alta y no se conserva como credencial de acceso ni como dato persistente del sistema.',
],
},
{
heading: 'Disponibilidad, conservación y soporte',
body: [
`${controller.platformName} podrá conservar documentos firmados, evidencias técnicas, registros de auditoría, comprobantes de aceptación legal y demás soportes durante el tiempo necesario para prestar el servicio, atender reclamaciones y cumplir obligaciones legales aplicables.`,
`Las consultas, incidentes y solicitudes relacionadas con la cuenta, el servicio o los documentos legales pueden remitirse a ${controller.contactEmail}.`,
],
},
],
},
{
slug: 'privacy-policy',
title: 'Política de Privacidad y Aviso de Tratamiento de Datos',
version: LEGAL_VERSION,
effectiveDate: EFFECTIVE_DATE,
summary: `Información sobre tratamiento de datos personales conforme a la LOPDP para personas usuarias de ${controller.platformName}.`,
sections: [
{
heading: 'Responsable del tratamiento',
body: [
`El responsable del tratamiento es ${controller.controllerName}, operador de ${controller.platformName}.`,
`Datos de contacto del responsable: correo ${controller.contactEmail}, teléfono ${controller.contactPhone}, dirección ${controller.contactAddress} y sitio web ${controller.website}.`,
],
},
{
heading: 'Datos personales tratados',
body: [
'Se pueden tratar datos identificativos y de contacto, correo electrónico, credenciales de acceso cifradas, datos del certificado digital, dirección IP, agente de usuario, registros de auditoría, archivos cargados, documentos firmados, metadatos de firma y evidencia técnica vinculada al servicio.',
'Durante el alta legal también se trata el archivo del certificado digital y la contraseña del certificado exclusivamente para validar el certificado y firmar los documentos legales iniciales.',
],
},
{
heading: 'Finalidades y base jurídica',
body: [
`Los datos se tratan para crear y administrar la cuenta, autenticar a la persona usuaria, habilitar firma electrónica, administrar certificados, generar y conservar evidencia de operaciones, atender solicitudes, enviar notificaciones y cumplir obligaciones legales, regulatorias o contractuales.`,
'La base jurídica del tratamiento podrá ser la ejecución de la relación precontractual o contractual, el cumplimiento de obligaciones legales y, cuando corresponda, el consentimiento expreso, libre, específico, informado e inequívoco de la persona titular.',
],
},
{
heading: 'Conservación y consecuencias de la entrega de datos',
body: [
'Los datos se conservarán mientras la cuenta permanezca activa y, posteriormente, durante el tiempo necesario para cumplir obligaciones legales, atender reclamaciones, resguardar evidencias y cumplir políticas de conservación documental y de seguridad.',
`Si la persona titular no proporciona los datos mínimos requeridos o no entrega un certificado digital válido para el alta legal inicial, ${controller.platformName} no podrá completar el registro ni habilitar las funciones de firma electrónica que dependan de dicho certificado.`,
],
},
{
heading: 'Destinatarios, encargados y transferencias',
body: [
'Los datos podrán ser tratados por proveedores tecnológicos que actúen por cuenta del responsable para servicios de hosting, almacenamiento, correo transaccional, seguridad, soporte o respaldo, siempre bajo instrucciones documentadas y medidas de seguridad adecuadas.',
'No se realizan decisiones basadas únicamente en tratamientos automatizados con efectos jurídicos para la persona titular ni perfiles comerciales automatizados en el marco de este flujo de registro. Si existieran transferencias nacionales o internacionales necesarias para la prestación del servicio o por obligación legal, se realizarán con las salvaguardas exigidas por la normativa aplicable.',
],
},
{
heading: 'Derechos de la persona titular y canal de atención',
body: [
'La persona titular puede ejercer los derechos previstos en la LOPDP, incluyendo acceso, rectificación, actualización, eliminación, oposición, portabilidad, suspensión del tratamiento y revocatoria del consentimiento cuando sea jurídicamente procedente.',
`Las solicitudes para ejercicio de derechos, consultas o reclamos pueden enviarse a ${controller.rightsEmail}. ${controller.controllerName} atenderá dichas solicitudes conforme a los plazos y requisitos establecidos en la normativa aplicable.`,
],
},
],
},
{
slug: 'data-processing-consent',
title: 'Consentimiento Expreso para Tratamiento de Datos y Uso de Firma Electrónica',
version: LEGAL_VERSION,
effectiveDate: EFFECTIVE_DATE,
summary: 'Aceptación expresa del tratamiento de datos personales, del uso del certificado digital para el alta inicial y de la generación de evidencia legal del registro.',
sections: [
{
heading: 'Declaración de consentimiento',
body: [
`La persona usuaria declara de forma expresa, libre, específica, informada e inequívoca que ha leído y comprendido los Términos de Uso, la Política de Privacidad y este documento, y que acepta el tratamiento de sus datos personales por ${controller.controllerName} para operar dentro de ${controller.platformName}.`,
'La persona usuaria autoriza el uso del certificado digital cargado exclusivamente para firmar los documentos legales del alta inicial, generar evidencia de aceptación y habilitar el flujo de registro dentro de la plataforma.',
],
},
{
heading: 'Uso del certificado y de la contraseña del certificado',
body: [
'La contraseña del archivo P12 o PFX se utilizará únicamente durante el proceso técnico de validación del certificado y firma de los documentos legales iniciales.',
'La contraseña del certificado no será almacenada por la plataforma como credencial persistente ni será reutilizada automáticamente para otras operaciones posteriores.',
],
},
{
heading: 'Evidencia y entrega de copias',
body: [
'La aceptación queda respaldada por los documentos legales firmados electrónicamente, por el correo registrado, por la fecha y hora del registro, por la huella digital del archivo firmado y por los registros técnicos asociados a la operación.',
`La persona usuaria recibirá una copia de los documentos legales firmados en el correo registrado y podrá consultarlos posteriormente dentro de ${controller.platformName} en la sección Legal, en modo de solo lectura.`,
],
},
{
heading: 'Revocatoria y límites',
body: [
`La persona titular podrá solicitar información adicional o ejercer sus derechos mediante ${controller.rightsEmail}. La revocatoria del consentimiento, cuando resulte jurídicamente procedente, no afectará la licitud de los tratamientos realizados con anterioridad ni la conservación obligatoria de evidencias y soportes legales.`,
],
},
],
},
];
};
module.exports = {
LEGAL_VERSION,
EFFECTIVE_DATE,
getLegalTemplates,
};
+2 -1
View File
@@ -31,7 +31,7 @@ const getTransporter = () => {
return transporter;
};
const sendMail = async ({ to, subject, html, text }) => {
const sendMail = async ({ to, subject, html, text, attachments }) => {
const activeTransporter = getTransporter();
const info = await activeTransporter.sendMail({
from: env.mail.from,
@@ -39,6 +39,7 @@ const sendMail = async ({ to, subject, html, text }) => {
subject,
html,
text,
attachments,
});
return info;
+6 -2
View File
@@ -1,7 +1,7 @@
const env = require('../../config/env');
const mailRepository = require('./mail.repository');
const sendVerificationEmail = async ({ name, email, verificationToken }) => {
const sendVerificationEmail = async ({ name, email, verificationToken, attachments = [] }) => {
const verificationUrl = `${env.frontendUrl.replace(/\/$/, '')}/auth/verify-email/${verificationToken}`;
const subject = 'Confirma tu cuenta de LinkedSign';
const text = [
@@ -10,8 +10,10 @@ const sendVerificationEmail = async ({ name, email, verificationToken }) => {
'Confirma tu cuenta de LinkedSign usando este enlace:',
verificationUrl,
'',
attachments.length > 0 ? 'Adjuntamos una copia de los documentos legales firmados durante tu registro.' : '',
'',
'Si no solicitaste esta cuenta, puedes ignorar este mensaje.',
].join('\n');
].filter(Boolean).join('\n');
const html = `
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #111827;">
<h2 style="margin-bottom: 12px;">Confirma tu cuenta de LinkedSign</h2>
@@ -24,6 +26,7 @@ const sendVerificationEmail = async ({ name, email, verificationToken }) => {
</p>
<p>Si el botón no funciona, copia y pega este enlace en tu navegador:</p>
<p><a href="${verificationUrl}">${verificationUrl}</a></p>
${attachments.length > 0 ? '<p>Adjuntamos una copia de los documentos legales firmados durante tu registro inicial.</p>' : ''}
<p>Si no solicitaste esta cuenta, puedes ignorar este mensaje.</p>
</div>
`;
@@ -33,6 +36,7 @@ const sendVerificationEmail = async ({ name, email, verificationToken }) => {
subject,
html,
text,
attachments,
});
if (!env.mail.user || !env.mail.pass) {
@@ -222,6 +222,8 @@ const signPdf = async ({
qrBackground = 'transparent',
qrBorder = 'none',
verificationUrl = null,
outputDir = null,
outputBaseName = null,
}) => {
const pdfBytes = await fs.readFile(pdfPath);
const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
@@ -293,9 +295,9 @@ const signPdf = async ({
const signedBuf = await signInstance.sign(pdfWithPlaceholder, signer);
// ── Save ──────────────────────────────────────────────────────────
const destDir = await getSignedDocumentPath();
const destDir = outputDir || await getSignedDocumentPath();
const ext = path.extname(pdfPath);
const base = path.basename(pdfPath, ext).replace(/^\d+-/, '');
const base = outputBaseName || path.basename(pdfPath, ext).replace(/^\d+-/, '');
const nowForFilename = new Date();
const year = String(nowForFilename.getFullYear());
const month = String(nowForFilename.getMonth() + 1).padStart(2, '0');
@@ -303,7 +305,7 @@ const signPdf = async ({
const hours = String(nowForFilename.getHours()).padStart(2, '0');
const minutes = String(nowForFilename.getMinutes()).padStart(2, '0');
const dateSuffix = `${year}${month}${day}${hours}${minutes}`;
const signedFilename = `${base}_${dateSuffix}${ext}`;
const signedFilename = outputBaseName ? `${base}${ext}` : `${base}_${dateSuffix}${ext}`;
const signedPath = path.join(destDir, signedFilename);
await fs.writeFile(signedPath, signedBuf);