Initial LinkedSign platform

This commit is contained in:
2026-04-06 20:33:59 -05:00
commit 1074c7c9d2
159 changed files with 19072 additions and 0 deletions
+286
View File
@@ -0,0 +1,286 @@
const bcrypt = require('bcrypt');
const { v4: uuidv4 } = require('uuid');
const authRepository = require('./auth.repository');
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 { signAccess, signRefresh } = require('../../utils/jwt');
const SALT_ROUNDS = 12;
const buildDisplayNameFromEmail = (email) => {
const localPart = String(email).split('@')[0] || 'Usuario';
const normalized = localPart
.replace(/[._-]+/g, ' ')
.trim()
.slice(0, 100);
return normalized || 'Usuario';
};
const issueVerificationEmail = async (user) => {
const verificationToken = uuidv4();
const verificationTokenExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
await authRepository.updateVerificationToken(user.id, verificationToken, verificationTokenExpires);
const mailResult = await mailService.sendVerificationEmail({
name: user.name || buildDisplayNameFromEmail(user.email),
email: user.email,
verificationToken,
});
return {
verificationToken,
verificationUrl: mailResult.verificationUrl,
};
};
const register = async ({ email, password }) => {
const existing = await authRepository.findByEmail(email);
if (existing) {
const err = new Error('Email already registered.');
err.statusCode = 409;
throw err;
}
const name = buildDisplayNameFromEmail(email);
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
const verificationToken = uuidv4();
const verificationTokenExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
const userId = await authRepository.createUser({
name,
email,
passwordHash,
verificationToken,
verificationTokenExpires,
});
let mailResult;
try {
mailResult = await mailService.sendVerificationEmail({
name,
email,
verificationToken,
});
} 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';
throw err;
}
await auditService.logEvent({
type: 'USER_REGISTERED',
userId,
email,
metadata: {
verificationEmailSent: true,
},
});
return { userId, verificationToken, verificationUrl: mailResult.verificationUrl };
};
const login = async ({ email, password }, context = {}) => {
const user = await authRepository.findByEmail(email.trim().toLowerCase());
if (!user || !user.is_active) {
await auditService.logEvent({ type: 'LOGIN_FAILED', email, ip: context.ip, userAgent: context.userAgent });
const err = new Error('Invalid credentials.');
err.statusCode = 401;
throw err;
}
const passwordMatch = await bcrypt.compare(password, user.password_hash);
if (!passwordMatch) {
await auditService.logEvent({ type: 'LOGIN_FAILED', userId: user.id, email, ip: context.ip, userAgent: context.userAgent });
const err = new Error('Invalid credentials.');
err.statusCode = 401;
throw err;
}
if (!user.is_verified) {
let verificationUrl = null;
try {
({ verificationUrl } = await issueVerificationEmail(user));
} catch (error) {
const err = new Error('Your email is not verified and the verification email could not be resent. Please contact support.');
err.statusCode = 503;
err.code = 'MAIL_SEND_FAILED';
throw err;
}
await auditService.logEvent({
type: 'VERIFICATION_EMAIL_RESENT',
userId: user.id,
email: user.email,
ip: context.ip,
userAgent: context.userAgent,
metadata: { source: 'login' },
});
const err = new Error('Your email is not verified yet. A new verification link has been sent to your email.');
err.statusCode = 403;
err.code = 'EMAIL_NOT_VERIFIED';
err.data = { verificationUrl };
throw err;
}
const payload = { sub: user.id, email: user.email, role: user.role, name: user.name };
const accessToken = signAccess(payload);
const refreshToken = signRefresh(payload);
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await authRepository.saveRefreshToken(user.id, refreshToken, expiresAt);
await auditService.logEvent({ type: 'LOGIN_SUCCESS', userId: user.id, email, ip: context.ip, userAgent: context.userAgent });
const signaturePreferences = await usersService.getSignaturePreferences(user.id);
return {
accessToken,
refreshToken,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
signaturePreferences,
},
};
};
const refresh = async (token) => {
const record = await authRepository.findRefreshToken(token);
if (!record) {
const err = new Error('Invalid or expired refresh token.');
err.statusCode = 401;
throw err;
}
if (!record.is_active) {
await authRepository.deleteUserRefreshTokens(record.user_id);
const err = new Error('Session invalidated.');
err.statusCode = 401;
throw err;
}
await authRepository.deleteRefreshToken(token);
const payload = { sub: record.uid, email: record.email, role: record.role };
const accessToken = signAccess(payload);
const newRefreshToken = signRefresh(payload);
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await authRepository.saveRefreshToken(record.uid, newRefreshToken, expiresAt);
return { accessToken, refreshToken: newRefreshToken };
};
const logout = async (token) => {
await authRepository.deleteRefreshToken(token);
};
const changePassword = async (userId, { currentPassword, newPassword }, context = {}) => {
const user = await authRepository.findAuthUserById(userId);
if (!user || !user.is_active) {
const err = new Error('User not found.');
err.statusCode = 404;
throw err;
}
const passwordMatch = await bcrypt.compare(currentPassword, user.password_hash);
if (!passwordMatch) {
const err = new Error('Current password is incorrect.');
err.statusCode = 401;
throw err;
}
const isSamePassword = await bcrypt.compare(newPassword, user.password_hash);
if (isSamePassword) {
const err = new Error('The new password must be different from the current password.');
err.statusCode = 400;
throw err;
}
const passwordHash = await bcrypt.hash(newPassword, SALT_ROUNDS);
const confirmationToken = uuidv4();
const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
await passwordChangeRepository.saveRequest(confirmationToken, {
userId,
email: user.email,
passwordHash,
expiresAt: expiresAt.toISOString(),
});
await mailService.sendPasswordChangeConfirmationEmail({
name: user.name || buildDisplayNameFromEmail(user.email),
email: user.email,
confirmationToken,
});
await auditService.logEvent({
type: 'PASSWORD_CHANGE_REQUESTED',
userId,
email: user.email,
ip: context.ip,
userAgent: context.userAgent,
});
return { confirmationToken };
};
const confirmPasswordChange = async (token) => {
const request = await passwordChangeRepository.findRequest(token);
if (!request) {
const err = new Error('Invalid or expired password change token.');
err.statusCode = 400;
throw err;
}
if (new Date(request.expiresAt) < new Date()) {
await passwordChangeRepository.deleteRequest(token);
const err = new Error('Invalid or expired password change token.');
err.statusCode = 400;
throw err;
}
const user = await authRepository.findAuthUserById(request.userId);
if (!user || !user.is_active) {
await passwordChangeRepository.deleteRequest(token);
const err = new Error('User not found.');
err.statusCode = 404;
throw err;
}
await authRepository.updatePassword(request.userId, request.passwordHash);
await passwordChangeRepository.deleteRequest(token);
await auditService.logEvent({
type: 'PASSWORD_CHANGED',
userId: user.id,
email: user.email,
});
try {
await mailService.sendPasswordChangedEmail({
name: user.name || buildDisplayNameFromEmail(user.email),
email: user.email,
changedAt: new Date(),
});
} catch (error) {
console.error('[PASSWORD_CHANGED_EMAIL_FAILED]', error.message);
}
};
const verifyEmail = async (token) => {
const userId = await authRepository.verifyEmail(token);
if (!userId) {
const err = new Error('Invalid or expired verification token.');
err.statusCode = 400;
throw err;
}
return userId;
};
module.exports = { register, login, refresh, logout, verifyEmail, changePassword, confirmPasswordChange };