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