Files
turnosxpress/server/src/Models/Users/Users.ts
T

1314 lines
48 KiB
TypeScript

import fs from "fs";
import path from "path";
import { rm } from "fs/promises";
import axios from "axios";
import {
IUser,
IUserView,
SignUpParams,
LoginParams,
LoginByTokenParams,
LoginByGoogleParams,
SetUserFileParams,
VerificationUserParams,
PaginateUsersParams,
PaginateUsersResults,
UpdateUserParams,
FindUsersParams,
ChangePasswordParams,
CollaboratorView,
FindCollaboratorByEmailParams,
RecoveryAccountParams,
IGoogleData,
DeleteUserParams,
SavePayerEmailParams,
GetPayerEmailParams,
PayerEmailResult,
CheckUserPhoneParams,
UserAssistanceInfo,
SysAdminPaginateUsersParams,
SysAdminUpdateProfileParams,
SysAdminSetVerifiedParams,
SysAdminDeleteUserParams,
SysAdminOrganizationsStatusParams,
SysAdminUserOrganizationsResult,
} from "./Users.Interface";
import { UsersMongooseAdapter } from "./Users.Adapter.Mongoose";
import { IUsersAdapter, IUsersManager } from "./Users.Interface";
import { ISession } from "../Session.model";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import Joi from "joi";
import hashPassword from "../../helpers/HashPassword";
import { isNull } from "../../helpers/IsNull";
import { joinStrings } from "../../helpers/String";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { NotificationsManager } from "../Notifications/Notifications";
import sharp from "sharp";
import getAvatar from "../../helpers/getAvatar";
import PlanSubscriptionsList from "../../Models/PlanSubscriptions/PlanSubscriptons";
import { ISubscriptionInfo } from "../PlanSubscriptions/PlanSubscriptions.interface";
import MetricsList from "../Metrics/Metrics";
import PlansList from "../Plans/Plans";
import CompaniesList from "../Companies/Companies";
import EmployeesList from "../Employees/Employee";
import SchedulesList from "../Schedules/Schedules";
import SchedulesEnabledList from "../SchedulesEnabled/SchedulesEnabled";
import EmployeesServicesList from "../EmployeesServices/EmployeesServices";
import AppointmentList from "../Appointments/Appointments";
import MessageConversationList from "../MessageConversations/MessagesConversations";
import SystemNoficationsList from "../SystemNotifications/SystemNotification";
import HeatMapList from "../HeatMap/HeatMap";
import ClientsList from "../Clients/Clients";
import RepeatsList from "../Repeats/Repeats";
import { OAuth2Client } from "google-auth-library";
import SchedulesDisabledList from "../SchedulesDisabled/SchedulesDisabled";
const cleanEmail = (email: string): string => {
return email.toLowerCase().trim();
};
class UsersManager implements IUsersManager {
users: IUsersAdapter;
constructor() {
this.users = new UsersMongooseAdapter();
}
private async getOrganizationSubscriptions(
userId: string
): Promise<Record<string, ISubscriptionInfo | null>> {
const subscriptions: Record<string, ISubscriptionInfo | null> = {};
// 1. Get companies where user is owner
const ownerCompanies = await CompaniesList.companies.find({ ownerId: userId });
for (const company of ownerCompanies) {
if (company.id) {
const sub = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: company.ownerId,
});
subscriptions[company.id] = sub;
}
}
// 2. Get companies where user is employee
const employeeRecords = await EmployeesList.employees.find({ userId: userId });
for (const employee of employeeRecords) {
const company = await CompaniesList.companies.findOne({ _id: employee.companyId });
if (company && company.id && !subscriptions[company.id]) {
const sub = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: company.ownerId,
});
subscriptions[company.id] = sub;
}
}
return subscriptions;
}
public async checkUserPhone(data: CheckUserPhoneParams): Promise<boolean> {
const sessionUser = await this.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
if (!sessionUser.phoneAreaCode || sessionUser.phoneAreaCode == "") {
return false;
}
if (!sessionUser.phoneNumber || sessionUser.phoneNumber == "") {
return false;
}
return true;
}
public async getPayerEmail(data: GetPayerEmailParams): Promise<PayerEmailResult> {
const sessionUser = await this.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
return {
mpPayerEmail: sessionUser.mpPayerEmail ? cleanEmail(sessionUser.mpPayerEmail) : "",
};
}
public async savePayerEmail(data: SavePayerEmailParams): Promise<void> {
const { email } = data;
// Define a validation profile using Joi
const validationProfile = Joi.object({
email: Joi.string().email().required(),
sessionUser: Joi.string().required(),
});
// Validate the data using the validation profile
const { error } = validationProfile.validate(data, {
abortEarly: false,
});
// Throw an error if validation fails
if (error) {
throw new Error(error.message);
}
const sessionUser = await this.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
sessionUser.mpPayerEmail = cleanEmail(email);
await sessionUser.save();
}
public async deleteUser(data: DeleteUserParams): Promise<void> {
const userCheck = await this.users.findOne({ _id: data.sessionUser });
if (!userCheck) {
throw new Error("El usuario que intenta dar de baja no existe.");
}
const ownerOrgs = await CompaniesList.companies.find({
ownerId: data.sessionUser,
});
//No puede tener organizaciones creadas.
if (ownerOrgs.length > 0) {
throw new Error(
"Para darte de baja primero tenes que eliminar las organizaciones que creaste."
);
}
//No debe tener una suscripcion.
const sub = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: data.sessionUser,
});
if (sub) {
if (sub.plan.price > 0) {
throw new Error("No podés darte de baja mientras tengas un plan pago vigente. Podés eliminar tus organizaciones para borrar su contenido y seguir usando la cuenta hasta el vencimiento.");
}
}
try {
//Si es colaborador en una o mas organizaciones, tenemos que:
const employees = await EmployeesList.employees.find({
userId: data.sessionUser,
});
for (const employee of employees) {
const organization = await CompaniesList.companies.findOne({
_id: employee.companyId,
});
if (!organization) {
continue;
}
const collaboratorUser = await this.users.findOne({
_id: employee.userId,
});
if (!collaboratorUser) {
continue;
}
//1. Eliminar sus horarios de la organizacion.
await SchedulesList.deleteSchedulesByEmployee({
employeeId: String(employee.id),
});
//2. Eliminar los horarios habilitados de la organizacion.
await SchedulesEnabledList.deleteSchedulesEnabledByEmployee({
employeeId: String(employee.id),
});
//3. Eliminar los horarios deshabilitados de la organizacion.
await SchedulesDisabledList.deleteSchedulesDisabledByEmployee({
employeeId: String(employee.id),
});
//3. Eliminar los servicios asignados al colaborador.
await EmployeesServicesList.deleteEmployeeServiceByEmployee({
employeeId: String(employee.id),
});
//4. Cambiar los turnos que tenga al duenio de la organizacion.
const appointments = await AppointmentList.Appointments.find({
employeeId: String(employee.id),
});
for (const appointment of appointments) {
await AppointmentList.changeEmployeeToOwner({
appointmentId: String(appointment.id),
companyId: String(employee.companyId),
});
}
//5. Eliminar el HeatMap del Colaborador.
await HeatMapList.deleteHeatMapByEmployee({
employeeId: String(employee.id),
});
//6. Eliminar el colaborador de la organizacion.
await EmployeesList.deleteEmployee({
employeeId: String(employee.id),
});
//7.Recalcular las metricas del duenio.
await MetricsList.calculateMetrics({
userId: String(organization.ownerId),
});
//Enviamos la notificacion al duenio de la organizacion a la que pertenecia el colaborador.
await SystemNoficationsList.createNotification({
userId: String(organization.ownerId),
subject: "Colaborador eliminado",
message: `El colaborador ${UserList.getUserFullName(
collaboratorUser
)} ha sido eliminado de tu organización. y sus turnos se han transferido a ti ya que se ha dado de baja de la plataforma.`,
});
}
//Eliminar las repeticiones de turnos.
await RepeatsList.deleteRepeatsByUser({
userId: data.sessionUser,
});
//Desvincular los clientes creados con el usuario sin borrar el historial de la organizacion.
const clients = await ClientsList.clients.find({
userId: data.sessionUser,
});
for (const client of clients) {
if (!client.id) {
continue;
}
await ClientsList.clients.detachUser({ clientId: String(client.id) });
}
//Eliminar las conversaciones
await MessageConversationList.deleteConversationsByUser({
userId: data.sessionUser,
});
//Eliminar las suscripciones
await PlanSubscriptionsList.deletePlanSuscriptionByUser({
sessionUser: data.sessionUser,
});
//Eliminar las notificaciones
await SystemNoficationsList.deleteNotificationsByUser({
userId: data.sessionUser,
});
//Eliminar foto de perfil.
await this.deleteUserFiles(data.sessionUser);
//Elimminar el usuario.
await this.users.delete(data.sessionUser);
} catch (e) {
console.log("Error al eliminar el usuario:", e);
}
}
private async deleteUserFiles(userId: string): Promise<void> {
const rootDir =
process.env.NODE_ENV !== "production"
? process.env.UPLOAD_USERS_FILES_ROOT_DEV
: process.env.UPLOAD_USERS_FILES_ROOT_PRO;
if (!rootDir) {
throw new Error("No se ha encontrado la ruta de los archivos");
}
const userDir = path.join(__dirname, rootDir, userId);
try {
await rm(userDir, { recursive: true, force: true });
} catch (err) {
console.error(`Error al eliminar el directorio ${userDir}:`, err);
}
}
public getUserFullName(data: IUser): string {
return joinStrings([data.firstName, data.lastName], " ");
}
private getVerificationCode(): string {
return Math.floor(Math.random() * 1000000000)
.toString()
.substring(0, 6);
}
public async signUp(data: SignUpParams): Promise<IUser> {
// Destructure email and password from the data object
const {
email,
password,
firstName,
lastName,
phoneCountryCode = "549",
phoneAreaCode,
phoneNumber,
} = data;
// Define a validation profile using Joi
const validationProfile = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().alphanum().min(8).required(),
firstName: Joi.string().required(),
lastName: Joi.string().required(),
phoneAreaCode: Joi.string().required(),
phoneNumber: Joi.string().required(),
});
// Validate the data using the validation profile
const { error } = validationProfile.validate(data, {
abortEarly: false,
});
// Throw an error if validation fails
if (error) {
throw new Error(error.message);
}
const user = await this.users.findOne({ email: cleanEmail(email) });
if (user) {
throw new Error("El usuario ya se encuentra registrado.");
}
const passwordCifrado = await hashPassword(password);
const verificationCode = this.getVerificationCode();
const newUserData: IUser = {
email: cleanEmail(email),
firstName: firstName,
lastName: lastName,
phoneCountryCode: phoneCountryCode,
phoneAreaCode: phoneAreaCode,
phoneNumber: phoneNumber,
password: passwordCifrado,
verificationCode: verificationCode,
};
const newUser = await this.users.create(newUserData);
const signUpUrl = "https://turnosxpress.com.ar/landing/login/verification";
await NotificationsManager.sendEmail({
email: cleanEmail(email),
subject: "TurnosXpress :: Bienvenido/a",
message: `Hola ${firstName}, Gracias por registrarte en turnosXpress. Primero debes activar tu cuenta ingresando en ${signUpUrl} . Tu código de verificación es: ${verificationCode}`,
});
return newUser;
}
public async recoveryAccount(data: RecoveryAccountParams): Promise<void> {
const validationProfile = Joi.object({
email: Joi.string().email().required().messages({
"string.email": "El correo ingresado no es valido",
"any.required": "El correo es requerido",
"string.empty": "El correo es requerido",
}),
recoveryCode: Joi.string().length(6).optional().messages({
"string.length": "El código de verificacion debe tener 6 digitos",
}),
newPassword: Joi.string().alphanum().min(8).optional(),
sendRecoveryCode: Joi.boolean().required().messages({
"any.required": "Faltan parametros requeridos",
"any.empty": "Faltan parametros requeridos",
}),
});
// Validate the data using the validation profile
const { error } = validationProfile.validate(data, {
abortEarly: false,
});
// Throw an error if validation fails
if (error) {
throw new Error(error.message);
}
const user = await this.users.findOne({ email: cleanEmail(data.email) });
if (!user) {
throw new Error("El email ingresado no se encuentra registrado");
}
if (data.sendRecoveryCode) {
const code = this.getVerificationCode();
user.recoveryCode = code;
await user.save();
await NotificationsManager.sendEmail({
email: cleanEmail(data.email),
subject: "TurnosXpress :: Recuperación de cuenta",
message: `Hola ${user.firstName}, Te enviamos un código de verificación para recuperar tu cuenta. Tu código de verificación es: ${code}. Ingresa https://turnosxpress.com.ar/landing/recover-account/verify para establecer una nueva clave`,
});
return;
}
if (!data.recoveryCode) {
throw new Error("Debes ingresar el código de verificación");
}
if (user.recoveryCode !== data.recoveryCode) {
throw new Error("El código de verificación es incorrecto");
}
if (!data.newPassword) {
throw new Error("Debes ingresar una nueva clave");
}
const passwordCifrado = await hashPassword(data.newPassword);
user.password = passwordCifrado;
user.recoveryCode = undefined;
await user.save();
}
public async verificateAndLoginUser(data: VerificationUserParams): Promise<ISession> {
const { email, password, verificationCode } = data;
// Define a validation profile using Joi
const validationProfile = Joi.object({
email: Joi.string().email().required(),
verificationCode: Joi.string().required(),
password: Joi.string().alphanum().min(8).required(),
});
// Validate the data using the validation profile
const { error } = validationProfile.validate(data, {
abortEarly: false,
});
// Throw an error if validation fails
if (error) {
throw new Error(error.message);
}
const userTest = await this.users.findOne({ email: cleanEmail(email) });
if (!userTest) {
throw new Error("Usuario no encontrado");
}
if (userTest.verificationCode !== verificationCode) {
throw new Error("El código de verificación es incorrecto");
}
userTest.verificated = true;
await userTest.save();
try {
const freePlan = await PlansList.plans.findOne({ price: 0 });
if (freePlan) {
const hasSub = await PlanSubscriptionsList.planSuscriptions.findOne({ sessionUser: String(userTest._id) });
if (!hasSub) {
const startDate = new Date();
const endDate = new Date(startDate.getTime() + 10 * 365 * 24 * 60 * 60 * 1000); // 10 years
await PlanSubscriptionsList.planSuscriptions.create({
userId: String(userTest._id),
planId: String(freePlan._id),
mpPlanId: String(freePlan._id),
startDate: startDate,
endDate: endDate,
autoRenew: true,
mpStatus: "authorized",
mpInitPoint: "/landing/dashboard"
});
}
}
} catch (e) {
console.error("Error asignando plan gratuito:", e);
}
return await this.login({
email: cleanEmail(email),
password: password,
});
}
public async login(data: LoginParams): Promise<ISession> {
const { email, password } = data;
// Define a validation profile using Joi
const validationProfile = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().alphanum().min(8).required(),
});
// Validate the data using the validation profile
const { error } = validationProfile.validate(data, {
abortEarly: false,
});
// Throw an error if validation fails
if (error) {
throw new Error(error.message);
}
const userTest = await this.users.findOne({ email: cleanEmail(email) });
if (!userTest) {
throw new Error("Usuario no encontrado");
}
if (!userTest.password) {
throw new Error("Clave no proporcionada");
}
const passwordTest = await bcrypt.compare(password, userTest.password);
if (!passwordTest) {
throw new Error("Credenciales invalidas");
}
if (!userTest.verificated) {
throw new Error("Debes verificar tu cuenta. Revisa la bandeja de entrada de tu correo");
}
//....
userTest.lastLoginDate = new Date();
await userTest.save();
const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: userTest.id,
});
const metrics = await MetricsList.getMetrics(userTest.id);
const userFullName = joinStrings([userTest.firstName, userTest.lastName], " ");
// Create a payload object with session data
const payload: ISession = {
loged: true,
userEmail: cleanEmail(userTest.email),
userFullName: userFullName,
userId: isNull<string>(userTest.id, ""),
userToken: "",
avatar: getAvatar(userTest.id, userTest.avatar, userFullName),
subscription: subscription,
organizationSubscriptions: await this.getOrganizationSubscriptions(userTest.id),
metrics: metrics,
};
//console.log(payload);
// Generate a JWT token using the payload and a secret word
const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
expiresIn: `${process.env.TOKEN_SESSION_TIME}` as any,
});
// Create a user object by merging the payload and the token
const user: ISession = { ...payload, ...{ userToken: token } };
// Return the user object
return user;
}
public async loginByToken({ token }: LoginByTokenParams): Promise<ISession> {
try {
// Verify the token using the secret word. This also decodes it.
const decodedPayload = jwt.verify(token, `${process.env.SECRET_WORD}`) as ISession & {
iat: number;
exp: number;
};
const userTest = await this.users.findOne({
email: cleanEmail(decodedPayload.userEmail),
});
if (!userTest) {
throw new Error("Ha ocurrido un error al reestablecer la sesión");
}
const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: decodedPayload.userId,
});
const metrics = await MetricsList.getMetrics(decodedPayload.userId);
const userFullName = joinStrings([userTest.firstName, userTest.lastName], " ");
// Create the session payload. This ensures data is up-to-date.
const payload: ISession = {
loged: true,
userEmail: cleanEmail(userTest.email),
userFullName: userFullName,
userId: isNull<string>(userTest.id, ""),
userToken: "", // Will be filled next
avatar: getAvatar(userTest.id, userTest.avatar, userFullName),
subscription: subscription,
organizationSubscriptions: await this.getOrganizationSubscriptions(decodedPayload.userId),
metrics: metrics,
};
const nowInSeconds = Math.floor(Date.now() / 1000);
const fiveMinutesInSeconds = 5 * 60;
let finalToken = token; // Assume we reuse the old token
// If the token expires in less than 5 minutes, generate a new one.
if (decodedPayload.exp < nowInSeconds + fiveMinutesInSeconds) {
finalToken = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
expiresIn: `${process.env.TOKEN_SESSION_TIME}` as any,
});
}
// Return the session object with the appropriate token
return { ...payload, userToken: finalToken };
} catch (error) {
// Throw an error if the token is invalid or expired
throw new Error("Invalid token");
}
}
private async getGoogleInfo(data: LoginByGoogleParams): Promise<IGoogleData> {
const profileResponse = await axios.get("https://www.googleapis.com/oauth2/v2/userinfo", {
headers: {
Authorization: `Bearer ${data.token}`,
},
});
return profileResponse.data as IGoogleData;
}
public async loginByAndroid(data: LoginByGoogleParams): Promise<ISession> {
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
if (!GOOGLE_CLIENT_ID) {
throw new Error("Error en el servidor, no se ha configurado el CLIENT_ID de Google");
}
const client = new OAuth2Client(GOOGLE_CLIENT_ID);
const ticket = await client.verifyIdToken({
idToken: data.token,
audience: GOOGLE_CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
});
const payload = ticket.getPayload();
if (!payload) {
throw new Error("Invalid token");
}
const googleUserId = payload.sub;
const email = payload.email;
const name = payload.name;
if (!email) {
throw new Error("Invalid token");
}
const userCheck = await this.users.findOne({
email: cleanEmail(email),
});
if (userCheck) {
userCheck.external_id = googleUserId;
userCheck.external_service = "google";
userCheck.verificated = true;
await userCheck.save();
const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: userCheck.id,
});
const metrics = await MetricsList.getMetrics(userCheck.id);
const userFullName = joinStrings([userCheck.firstName, userCheck.lastName], " ");
const payload: ISession = {
loged: true,
userEmail: cleanEmail(userCheck.email),
userFullName: userFullName,
userId: isNull<string>(String(userCheck.id), ""),
userToken: "",
avatar: getAvatar(userCheck.id, userCheck.avatar, userFullName),
subscription: subscription,
organizationSubscriptions: await this.getOrganizationSubscriptions(String(userCheck.id)),
metrics: metrics,
};
// Generate a JWT token using the payload and a secret word
const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
expiresIn: `${process.env.TOKEN_SESSION_TIME}` as any,
});
// Create a user object by merging the payload and the token
return { ...payload, ...{ userToken: token } };
} else {
const newUserData: IUser = {
email: cleanEmail(email),
firstName: name,
external_id: googleUserId,
external_service: "google",
};
const newUser = await this.users.create(newUserData);
if (!newUser.id) {
throw new Error("Error creating user");
}
const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: newUser.id,
});
const metrics = await MetricsList.getMetrics(newUser.id);
const userFullName = joinStrings([newUser.firstName, newUser.lastName], " ");
const payload: ISession = {
loged: true,
userEmail: cleanEmail(newUser.email),
userFullName: userFullName,
userId: isNull<string>(String(newUser.id), ""),
userToken: "",
avatar: getAvatar(
isNull<string>(String(newUser.id), ""),
newUser.avatar,
userFullName
),
subscription: subscription,
organizationSubscriptions: await this.getOrganizationSubscriptions(String(newUser.id)),
metrics: metrics,
};
// Generate a JWT token using the payload and a secret word
const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
expiresIn: `${process.env.TOKEN_SESSION_TIME}` as any,
});
// Create a user object by merging the payload and the token
return { ...payload, ...{ userToken: token } };
}
}
public async loginByGoogle(data: LoginByGoogleParams): Promise<ISession> {
try {
const googleData = await this.getGoogleInfo(data);
//TODO: creo que debo verificar por email.
// const userCheck = await this.users.findOne({
// external_id: googleId,
// external_service: "google",
// email: email,
// });
const userCheck = await this.users.findOne({
email: cleanEmail(googleData.email),
});
if (userCheck) {
userCheck.external_id = googleData.id;
userCheck.external_service = "google";
userCheck.verificated = true;
await userCheck.save();
const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: userCheck.id,
});
const metrics = await MetricsList.getMetrics(userCheck.id);
const userFullName = joinStrings([userCheck.firstName, userCheck.lastName], " ");
const payload: ISession = {
loged: true,
userEmail: cleanEmail(userCheck.email),
userFullName: userFullName,
userId: isNull<string>(String(userCheck.id), ""),
userToken: "",
avatar: getAvatar(userCheck.id, userCheck.avatar, userFullName),
subscription: subscription,
organizationSubscriptions: await this.getOrganizationSubscriptions(String(userCheck.id)),
metrics: metrics,
};
// Generate a JWT token using the payload and a secret word
const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
expiresIn: `${process.env.TOKEN_SESSION_TIME}` as any,
});
// Create a user object by merging the payload and the token
return { ...payload, ...{ userToken: token } };
} else {
const newUserData: IUser = {
email: cleanEmail(googleData.email),
firstName: googleData.name,
external_id: googleData.id,
external_service: "google",
};
const newUser = await this.users.create(newUserData);
if (!newUser.id) {
throw new Error("Error creating user");
}
const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: newUser.id,
});
const metrics = await MetricsList.getMetrics(newUser.id);
const userFullName = joinStrings([newUser.firstName, newUser.lastName], " ");
const payload: ISession = {
loged: true,
userEmail: cleanEmail(newUser.email),
userFullName: userFullName,
userId: isNull<string>(String(newUser.id), ""),
userToken: "",
avatar: getAvatar(
isNull<string>(String(newUser.id), ""),
newUser.avatar,
userFullName
),
subscription: subscription,
organizationSubscriptions: await this.getOrganizationSubscriptions(String(newUser.id)),
metrics: metrics,
};
// Generate a JWT token using the payload and a secret word
const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
expiresIn: `${process.env.TOKEN_SESSION_TIME}` as any,
});
// Create a user object by merging the payload and the token
return { ...payload, ...{ userToken: token } };
}
return {} as ISession;
} catch (error) {
console.log(error);
throw new Error("An error occurred while trying to login with Google");
}
}
public async changePassword(data: ChangePasswordParams): Promise<void> {
const sessionUser = await this.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
let passwordTest = false;
if (sessionUser.password) {
passwordTest = await bcrypt.compare(data.oldPassword, sessionUser.password);
}
if (!passwordTest) {
throw new Error("Credenciales invalidas");
}
if (!sessionUser.verificated) {
throw new Error("Debes verificar tu cuenta. Revisa la bandeja de entrada de tu correo");
}
const validationProfile = Joi.object({
newPassword: Joi.string().alphanum().min(8).required(),
});
// Validate the data using the validation profile
const { error } = validationProfile.validate(
{ newPassword: data.newPassword },
{
abortEarly: true,
}
);
// Throw an error if validation fails
if (error) {
throw new Error(error.message);
}
const passwordCifrado = await hashPassword(data.newPassword);
sessionUser.password = passwordCifrado;
sessionUser.recoveryCode = undefined;
await sessionUser.save();
await NotificationsManager.sendEmail({
email: cleanEmail(sessionUser.email),
subject: "TurnosXpress :: Contraseña actualizada",
message: `Hola ${sessionUser.firstName}, has actualizado tu contraseña con exito`,
});
}
public async paginate(filters: PaginateUsersParams): Promise<PaginateUsersResults> {
return await this.users.paginate(filters);
}
public async findOne(data: FindUsersParams): Promise<IUserView> {
const sessionUser = await this.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
return {
id: sessionUser.id,
email: cleanEmail(sessionUser.email),
firstName: sessionUser.firstName,
lastName: sessionUser.lastName,
fullName: this.getFullName(sessionUser),
avatar: getAvatar(sessionUser.id, sessionUser.avatar, this.getFullName(sessionUser)),
phoneNumber: isNull<string>(sessionUser.phoneNumber, ""),
phoneCountryCode: isNull<string>(sessionUser.phoneCountryCode, ""),
phoneAreaCode: isNull<string>(sessionUser.phoneAreaCode, ""),
formattedPhoneNumber: this.getFormattedPhoneNumber(sessionUser),
street: isNull<string>(sessionUser.street, ""),
streetNumber: isNull<string>(sessionUser.streetNumber, ""),
builingFloor: isNull<string>(sessionUser.builingFloor, ""),
buildingApartament: isNull<string>(sessionUser.buildingApartament, ""),
block: isNull<string>(sessionUser.block, ""),
city: isNull<string>(sessionUser.city, ""),
state: isNull<string>(sessionUser.state, ""),
country: isNull<string>(sessionUser.country, ""),
zipCode: isNull<string>(sessionUser.zipCode, ""),
};
}
public async findByEmail(
data: FindCollaboratorByEmailParams
): Promise<CollaboratorView | null> {
const user = await this.users.findOne({ email: cleanEmail(data.email) });
if (!user) {
return null;
}
return {
id: user.id,
email: cleanEmail(user.email),
fullName: this.getFullName(user),
avatar: getAvatar(user.id, user.avatar, this.getFullName(user)),
};
}
private getFormattedPhoneNumber(user: IUser): string {
return `(${isNull<string>(user.phoneCountryCode, "")})-${isNull<string>(
user.phoneAreaCode,
""
)}-${isNull<string>(user.phoneNumber, "")}`;
}
private getFullName(user: IUser): string {
return joinStrings([user.firstName, user.lastName], " ");
}
public async updateUser(data: UpdateUserParams): Promise<void> {
const sessionUser = await this.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
if (data.sessionUser != data.id) {
throw new Error(NoPermissionMessage());
}
await this.users.update(data);
await NotificationsManager.sendEmail({
email: cleanEmail(sessionUser.email),
subject: "TurnosXpress :: Usuario actualizado",
message: `Hola ${sessionUser.firstName}, has actualizado tu perfil con éxito`,
});
}
public async setUserIcon(data: SetUserFileParams): Promise<void> {
const sessionUser = await this.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
if (!data.file) {
throw new Error("No se ha enviado el archivo");
}
const rootDir =
process.env.NODE_ENV !== "production"
? process.env.UPLOAD_USERS_FILES_ROOT_DEV
: process.env.UPLOAD_USERS_FILES_ROOT_PRO;
const uploadPath = path.join(__dirname, rootDir + data.sessionUser);
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
const fileExtension = data.file.originalname.split(".").pop();
const newFileName = "avatar." + fileExtension;
const filePath = path.join(uploadPath, newFileName);
await sharp(data.file.buffer)
.resize({ width: 96, height: 96 })
.rotate()
.webp()
.toFile(filePath);
sessionUser.avatar = newFileName;
await sessionUser.save();
}
public async getUserAssistanceInfo(data: FindUsersParams): Promise<UserAssistanceInfo> {
const sessionUser = await this.users.findOne({ _id: data.sessionUser });
if (!sessionUser) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
if (!data.email) {
throw new Error("Debe proporcionar el email del usuario");
}
const returnValue: UserAssistanceInfo = {
message: "",
email: "",
fullName: "",
verificated: false,
activationCode: undefined,
recoveryCode: undefined,
};
const usserAssitance = await this.users.findOne({ email: cleanEmail(data.email) });
if (!usserAssitance) {
returnValue.message = "No se ha encontrado un usuario con ese email";
return returnValue;
}
if (usserAssitance.verificated && !usserAssitance.recoveryCode) {
returnValue.email = cleanEmail(usserAssitance.email);
returnValue.message =
"El usuario ya se encuentra activo. Si no puedes ingresar, intenta recuperar tu cuenta.";
returnValue.verificated = true;
return returnValue;
}
if (usserAssitance.verificated && usserAssitance.recoveryCode) {
returnValue.email = cleanEmail(usserAssitance.email);
returnValue.message =
"El usuario ya se encuentra activo y ha intentado cambiar su clave de acceso. Antes de proporcionar el código de recuperación, asegurate de verificar que se trate realmente del cliente, para evitar que una persona no autorizada acceda a su cuenta.";
returnValue.verificated = true;
returnValue.recoveryCode = usserAssitance.recoveryCode;
return returnValue;
}
returnValue.email = cleanEmail(usserAssitance.email);
returnValue.fullName = this.getFullName(usserAssitance);
returnValue.verificated = false;
returnValue.activationCode = usserAssitance.verificationCode;
returnValue.message = `El usuario no se encuentra activo.`;
return returnValue;
}
// --- SysAdmin Methods ---
public async sysAdminPaginateUsers(
data: SysAdminPaginateUsersParams
): Promise<PaginateUsersResults> {
return await this.users.sysAdminPaginate(data);
}
public async sysAdminUpdateProfile(data: SysAdminUpdateProfileParams): Promise<void> {
const { userId, ...updateData } = data;
const user = await this.users.findOne({ _id: userId });
if (!user) {
throw new Error("Usuario no encontrado");
}
await this.users.update({
id: userId,
sessionUser: userId, // Mocking sessionUser because update params require it
firstName: updateData.firstName,
lastName: updateData.lastName,
street: updateData.street,
streetNumber: updateData.streetNumber,
builingFloor: updateData.builingFloor,
buildingApartament: updateData.buildingApartament,
block: updateData.block,
city: updateData.city,
state: updateData.state,
country: updateData.country,
zipCode: updateData.zipCode,
phoneCountryCode: updateData.phoneCountryCode,
phoneAreaCode: updateData.phoneAreaCode,
phoneNumber: updateData.phoneNumber,
});
}
public async sysAdminSetVerifiedStatus(data: SysAdminSetVerifiedParams): Promise<void> {
const user = await this.users.findOne({ _id: data.userId });
if (!user) {
throw new Error("Usuario no encontrado");
}
user.verificated = data.isVerified;
if (data.isVerified) {
user.verificationCode = undefined;
}
await user.save();
}
public async sysAdminDeleteUser(data: SysAdminDeleteUserParams): Promise<void> {
const userCheck = await this.users.findOne({ _id: data.userId });
if (!userCheck) {
throw new Error("El usuario que intenta dar de baja no existe.");
}
const ownerOrgs = await CompaniesList.companies.find({
ownerId: data.userId,
});
//No puede tener organizaciones creadas.
if (ownerOrgs.length > 0) {
throw new Error(
"Para dar de baja al usuario primero tienes que eliminar las organizaciones que creó (manualmente)."
);
}
//No debe tener una suscripcion.
const sub = await PlanSubscriptionsList.getSubscriptionByUser({
sessionUser: data.userId,
});
if (sub) {
if (sub.plan.price > 0) {
throw new Error("Para dar de baja al usuario primero tienes que cancelar su suscripción (manualmente).");
}
}
try {
//Si es colaborador en una o mas organizaciones, tenemos que:
const employees = await EmployeesList.employees.find({
userId: data.userId,
});
for (const employee of employees) {
const organization = await CompaniesList.companies.findOne({
_id: employee.companyId,
});
if (!organization) continue;
const collaboratorUser = await this.users.findOne({
_id: employee.userId,
});
if (!collaboratorUser) continue;
await SchedulesList.deleteSchedulesByEmployee({ employeeId: String(employee.id) });
await SchedulesEnabledList.deleteSchedulesEnabledByEmployee({ employeeId: String(employee.id) });
await SchedulesDisabledList.deleteSchedulesDisabledByEmployee({ employeeId: String(employee.id) });
await EmployeesServicesList.deleteEmployeeServiceByEmployee({ employeeId: String(employee.id) });
const appointments = await AppointmentList.Appointments.find({ employeeId: String(employee.id) });
for (const appointment of appointments) {
await AppointmentList.changeEmployeeToOwner({
appointmentId: String(appointment.id),
companyId: String(employee.companyId),
});
}
await HeatMapList.deleteHeatMapByEmployee({ employeeId: String(employee.id) });
await EmployeesList.deleteEmployee({ employeeId: String(employee.id) });
await MetricsList.calculateMetrics({ userId: String(organization.ownerId) });
await SystemNoficationsList.createNotification({
userId: String(organization.ownerId),
subject: "Colaborador eliminado por SysAdmin",
message: `El colaborador ${this.getUserFullName(collaboratorUser)} ha sido dado de baja por la administración y sus turnos se han transferido a ti.`,
});
}
await RepeatsList.deleteRepeatsByUser({ userId: data.userId });
const clients = await ClientsList.clients.find({ userId: data.userId });
for (const client of clients) {
if (!client.id) continue;
await ClientsList.clients.detachUser({ clientId: String(client.id) });
}
await MessageConversationList.deleteConversationsByUser({ userId: data.userId });
await PlanSubscriptionsList.deletePlanSuscriptionByUser({ sessionUser: data.userId });
await SystemNoficationsList.deleteNotificationsByUser({ userId: data.userId });
await this.deleteUserFiles(data.userId);
await this.users.delete(data.userId);
} catch (e) {
console.log("Error al eliminar el usuario (SysAdmin):", e);
throw e;
}
}
public async sysAdminOrganizationsStatus(
data: SysAdminOrganizationsStatusParams
): Promise<SysAdminUserOrganizationsResult> {
const result: SysAdminUserOrganizationsResult = {
clientIn: [],
collaboratorIn: [],
};
const clients = await ClientsList.clients.find({ userId: data.userId });
for (const client of clients) {
const org = await CompaniesList.companies.findOne({ _id: String(client.companyId) });
if (org) {
result.clientIn.push({ companyId: String(org.id), companyName: org.name });
}
}
const employees = await EmployeesList.employees.find({ userId: data.userId });
for (const emp of employees) {
const org = await CompaniesList.companies.findOne({ _id: String(emp.companyId) });
if (org) {
result.collaboratorIn.push({ companyId: String(org.id), companyName: org.name });
}
}
return result;
}
}
const UserList = new UsersManager();
export default UserList;