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> { const subscriptions: Record = {}; // 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 { 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 { 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 { 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 { 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 { 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); } private async sendWelcomeEmail(email: string, firstName?: string): Promise { try { await NotificationsManager.sendEmail({ email: cleanEmail(email), templateId: "68139f23f3e003139b08b4f0", context: { username: firstName || cleanEmail(email), }, }); } catch (e) { if (axios.isAxiosError(e)) { console.error("Error enviando email de bienvenida:", { status: e.response?.status, data: e.response?.data, }); return; } console.error("Error enviando email de bienvenida:", e); } } public async signUp(data: SignUpParams): Promise { // 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); await NotificationsManager.sendEmail({ email: cleanEmail(email), templateId: "6a85dd644ea98ccfcd05bdc7", context: { username: firstName || cleanEmail(email), verificationcode: verificationCode, }, }); return newUser; } public async recoveryAccount(data: RecoveryAccountParams): Promise { 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), templateId: "6a85c7ad96475f2825015277", context: { username: user.firstName || cleanEmail(data.email), verificationcode: code, }, }); 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 { 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"); } const wasVerificated = Boolean(userTest.verificated); userTest.verificated = true; await userTest.save(); if (!wasVerificated) { await this.sendWelcomeEmail(userTest.email, userTest.firstName); } 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 { 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(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 { 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(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 { 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 { 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) { const wasVerificated = Boolean(userCheck.verificated); userCheck.external_id = googleUserId; userCheck.external_service = "google"; userCheck.verificated = true; await userCheck.save(); if (!wasVerificated) { await this.sendWelcomeEmail(userCheck.email, userCheck.firstName); } 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(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"); } await this.sendWelcomeEmail(newUser.email, newUser.firstName); 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(newUser.id), ""), userToken: "", avatar: getAvatar( isNull(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 { 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) { const wasVerificated = Boolean(userCheck.verificated); userCheck.external_id = googleData.id; userCheck.external_service = "google"; userCheck.verificated = true; await userCheck.save(); if (!wasVerificated) { await this.sendWelcomeEmail(userCheck.email, userCheck.firstName); } 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(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"); } await this.sendWelcomeEmail(newUser.email, newUser.firstName); 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(newUser.id), ""), userToken: "", avatar: getAvatar( isNull(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 { 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 { return await this.users.paginate(filters); } public async findOne(data: FindUsersParams): Promise { 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(sessionUser.phoneNumber, ""), phoneCountryCode: isNull(sessionUser.phoneCountryCode, ""), phoneAreaCode: isNull(sessionUser.phoneAreaCode, ""), formattedPhoneNumber: this.getFormattedPhoneNumber(sessionUser), street: isNull(sessionUser.street, ""), streetNumber: isNull(sessionUser.streetNumber, ""), builingFloor: isNull(sessionUser.builingFloor, ""), buildingApartament: isNull(sessionUser.buildingApartament, ""), block: isNull(sessionUser.block, ""), city: isNull(sessionUser.city, ""), state: isNull(sessionUser.state, ""), country: isNull(sessionUser.country, ""), zipCode: isNull(sessionUser.zipCode, ""), }; } public async findByEmail( data: FindCollaboratorByEmailParams ): Promise { 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(user.phoneCountryCode, "")})-${isNull( user.phoneAreaCode, "" )}-${isNull(user.phoneNumber, "")}`; } private getFullName(user: IUser): string { return joinStrings([user.firstName, user.lastName], " "); } public async updateUser(data: UpdateUserParams): Promise { 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 { 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 { 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 { return await this.users.sysAdminPaginate(data); } public async sysAdminUpdateProfile(data: SysAdminUpdateProfileParams): Promise { 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 { 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 { 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 { 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;