Initial LinkedSign platform
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user