import type { IDbUser } from "@interfaces/database/IDbUser"; import type { IReqLogin } from "@interfaces/requests/IReqLogin"; import type { IReqRegister } from "@interfaces/requests/IReqRegister"; import { ErrorType, type ISError } from "@interfaces/services/ISError"; import CredentialService from "@services/credential.service"; import JwtService from "@services/jwt.service"; import MySqlService from "@services/mysql.service"; import MysqlService from "@services/mysql.service"; import { Logger } from "tslog"; import { v4 } from "uuid"; const logger = new Logger({ name: "UserService", }); const DbHandler = new MySqlService.Handler("UserService"); /** * Retrieves a user from the database by the given email address. * * @param {string} targetEmail - The email address of the user to retrieve. * @returns {Promise} * - A promise that resolves with the user. * - If the user is not found, an error object is returned. * - If an error occurs during the database operation, an error object is returned. */ async function getUserByEmail(targetEmail: string): Promise { try { const dbUser = await MySqlService.User.getByEmail(DbHandler, targetEmail); if (dbUser === undefined) { logger.info(`User not found (${targetEmail})`); return { error: ErrorType.NotFound, message: "The user was not fund.", }; } return dbUser; } catch (err) { logger.error(err); return { error: ErrorType.DatabaseError, message: "An unknown error occurred.", }; } } /** * Retrieves a user from the database based on the provided ID. * * @param {string} id - The ID of the user to retrieve. * @returns {Promise} - A promise that resolves with the user object if found, or an error object if not. */ async function getUserFromIdService(id: string): Promise { try { if (!id || id.length !== 36) { logger.info(`Invalid ID (${id})`); return { error: ErrorType.InvalidData, message: "Invalid ID length.", }; } const dbUser = await MySqlService.User.getById(DbHandler, id); if (dbUser === undefined) { logger.info(`User not found (${id})`); return { error: ErrorType.NotFound, message: "The user was not found.", }; } return dbUser; } catch (err) { return { error: ErrorType.DatabaseError, message: "An unknown error occurred.", }; } } async function register(inputData: IReqRegister): Promise { if (inputData.password.length < 6) { return { error: ErrorType.InvalidData, message: "Password must be at least 6 characters long.", }; } //TODO check Object content keys const passwordHash = await CredentialService.hash(`${inputData.password}`); // Does the new user has accepted GDPR ? if (inputData.gdpr !== true) { return { error: ErrorType.InvalidData, message: "GDPR acceptance is required.", }; } const currentDate = new Date(); // Check if exist and return const dbUserIfExist: IDbUser | ISError = await getUserByEmail( inputData.email, ); if ("error" in dbUserIfExist) { return { error: dbUserIfExist.error, message: dbUserIfExist.message, }; } if (dbUserIfExist.id) { logger.info( `User already exist for email "${inputData.email}".\n(${dbUserIfExist.username}::${dbUserIfExist.id})\n`, ); return { error: ErrorType.UnAuthorized, message: "User already exists.", }; } const currentId = v4(); const NewUser = await MySqlService.User.insert(DbHandler, { id: currentId, email: inputData.email, username: inputData.username, firstname: inputData.firstName, lastname: inputData.lastName, dob: inputData.dob, hash: passwordHash, gdpr: currentDate, is_admin: false, is_mail_verified: false, }); if ("error" in NewUser || NewUser.affectedRows === 0) { return { error: ErrorType.DatabaseError, message: "Error when inserting user in database.", }; } logger.info(`New user created ! (${inputData.username}::${currentId})`); // JWT const token = await JwtService.sign( { sub: NewUser.id, }, { alg: "HS512", }, "1d", "user", ); return token; } async function login(ReqData: IReqLogin) { //const passwordHash = await getHashFromPassword(sanitizedData.password); const dbUser = await MysqlService.User.getByUsername( DbHandler, ReqData.username, ); if (!dbUser) { console.log(`LoginService :> User does not exist (${ReqData.username})`); return { error: "userNotFound", }; } if (ReqData.password.length < 6) { console.log("X"); console.log(`LoginService :> Invalid password (${ReqData.username})`); return { error: "invalidPassword", }; } const isPasswordValid = await CredentialService.compare( ReqData.password, dbUser.hash, ); if (!isPasswordValid) { console.log(isPasswordValid); console.log(`LoginService :> Invalid password (${ReqData.username})`); return { error: "invalidPassword", }; } // biome-ignore lint/style/useConst: let userData = { error: "none", jwt: "", user: { id: dbUser.id, username: dbUser.username, displayName: dbUser.displayName, }, }; userData.jwt = await JwtService.sign( { sub: dbUser.id, }, { alg: "HS512", }, "7d", "user", ); console.log("USERDATA :>"); console.log(userData); return userData; } /** * Retrieves all users from the database. * * @async * @function getAllUsersService * @returns {Promise<{iat: number, users: Array, length: number}>} - The response object containing the users array and its length. */ async function getAllUsersService() { const users = await Db.collection("users").find().toArray(); // biome-ignore lint/complexity/noForEach: users.forEach((user) => { delete user.passwordHash; delete user._id; delete user.gdpr; }); logger.info(`Query ${users.length} user(s)`); return { iat: Date.now(), users: users, length: users.length, }; } /** * Edits a user in the database. * * @param {string} targetId - The ID of the user to be edited. * @param {object} sanitizedData - The sanitized data to update the user with. * @returns {object} - An object indicating the result of the operation. * If the user is not found, the error property will be a string "userNotFound". * Otherwise, the error property will be a string "none". */ async function editUserService(targetId, sanitizedData) { if (sanitizedData.password) { const passwordHash = await getHashFromPassword(sanitizedData.password); delete sanitizedData.password; logger.info(`Changing password for user "${targetId}"`); sanitizedData.passwordHash = passwordHash; } const updatedUserResult = await Db.collection("users").updateOne( { id: targetId, }, { $set: sanitizedData, }, ); if (updatedUserResult.modifiedCount === 0) { logger.info(`EDIT :> User not found (${targetId})`); return { error: "userNotFound", }; } logger.info(`EDIT :> User updated (${targetId})`); return { error: "none", }; } /** * Delete a user from the database. * * @param {string} targetId - The ID of the user to be deleted. * @return {Promise} - A promise that resolves to true if the user is successfully deleted, or false if an error occurs. */ async function deleteUserService(targetId) { logger.info(`Deleting user ${targetId}`); try { await Db.collection("users").deleteOne({ id: targetId, }); return true; } catch (e) { logger.warn(e); return false; } } logger.debug("\nService loaded."); const UserService = { register: register, login: login, getAll: getAllUsersService, getFromId: getUserFromIdService, edit: editUserService, delete: deleteUserService, }; export default UserService;