From 2a33f4525767d15a029a1b77b4b94dc6df98bd85 Mon Sep 17 00:00:00 2001 From: Mathis Date: Tue, 23 Apr 2024 15:13:23 +0200 Subject: [PATCH] feat(services): :rocket: UserService --- src/services/user.service.ts | 234 +++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/services/user.service.ts diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000..f7e7228 --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,234 @@ +import {Logger} from "tslog"; + +import Argon2id from "@node-rs/argon2"; +import MySqlService from "@services/mysql.service"; + + +const logger = new Logger({ name: "UserService" }); + +const DbHandler = new MySqlService.Handler('UserService') + +/** + * Retrieves a user object from the database based on the given username. + * + * @param {string} username - The username of the user to retrieve. + * @returns {Promise} - The user object if found, or null if not found. + */ +async function getUserFromUsername(username: string): Promise { + const dbUser = await MySqlService.User.getByUsername(DbHandler, username) + if (dbUser === undefined) return null; + return dbUser; +} + +async function getUserFromIdService(id: string | undefined) { + const dbUser = await MySqlService.User.getById(DbHandler, id); + if (dbUser === undefined) return null; + return dbUser; +} + +/** + * Registers a new user by creating a UserService object, generating a JWT token, and inserting the user into the database. + * + * @param {Object} sanitizedData - The sanitized user data. + * @param {string} sanitizedData.username - The username of the new user. + * @param {string} sanitizedData.displayName - The display namcoe of the new user. + * @param {string} sanitizedData.firstName + * @param {string} sanitizedData.lastName + * @param {string} sanitizedData.password - The password of the new user. + * @param {boolean} sanitizedData.gdpr - Indicates whether the new user has accepted GDPR. + * + * @returns {Object} - An object containing the registered user's data and JWT token. + * @returns {string} error - The error name, if any. "none" if registration was successful. + * @returns {string|null} jwt - The JWT token for the registered user. Null if registration was not successful. + * @returns {Object|null} user - The registered user's data. Null if registration was not successful. + * @returns {string|null} user.id - The ID of the registered user. Null if registration was not successful. + * @returns {string|null} user.username - The username of the registered user. Null if registration was not successful. + * @returns {string|null} user.displayName - The display name of the registered user. Null if registration was not successful. + */ +async function RegisterService(sanitizedData) { + if (sanitizedData.password.length < 6) { + logger.info(`REGISTER :> Invalid password (${sanitizedData.username})`) + return { error: "invalidPassword" }; + } + const passwordHash = await getHashFromPassword(sanitizedData.password) + + // Does the new user has accepted GDPR ? + if (sanitizedData.gdpr !== true) { + logger.info(`REGISTER :> GDPR not validated (${sanitizedData.username})`) + return { error: "gdprNotApproved" } + } + + // Check if exist and return + + const dbUserIfExist = await getUserFromUsername(sanitizedData.username) + if (dbUserIfExist) { + logger.info(`REGISTER :> User exist (${dbUserIfExist.username})\n ID:${dbUserIfExist.id}`) + return { error: "exist" } + } + + const currentDate = new Date(); + + // New UserService (class) + + const NewUser = new User(sanitizedData.username, sanitizedData.displayName, passwordHash, currentDate); + NewUser.setFirstName(sanitizedData.firstName); + NewUser.setLastName(sanitizedData.lastName); + + // JWT + + const alg = 'HS512' + const token = await JwtSign({ + sub: NewUser.id + }, alg, + '1d', + 'user') + + const userData = { + error: "none", + jwt: token, + user: { + id: NewUser.id, + username: NewUser.username, + displayName: NewUser.displayName, + firstName: NewUser.firstName, + lastName: NewUser.lastName + }}; + logger.info(userData) + await Db.collection("users").insertOne(NewUser); + logger.info(`REGISTER :> Inserted new user (${NewUser.username})`) + return userData +} + +/** + * Performs the login process by verifying the provided credentials. + * @param {Object} sanitizedData - The sanitized user login data. + * @param {string} sanitizedData.username - The username provided by the user. + * @param {string} sanitizedData.password - The password provided by the user. + * @returns {Object} - The login result object. + * @returns {string} result.error - The error code if there is an error during the login process. + * @returns {string} result.jwt - The JSON Web Token (JWT) generated upon successful login. + * @returns {Object} result.user - The user information. + * @returns {number} result.user.id - The ID of the user. + * @returns {string} result.user.username - The username of the user. + * @returns {string} result.user.displayName - The display name of the user. + */ +async function LoginService(sanitizedData) { + //const passwordHash = await getHashFromPassword(sanitizedData.password); + const dbUser = await getUserFromUsername(sanitizedData.username); + if (!dbUser) { + console.log(`LoginService :> User does not exist (${sanitizedData.username})`); + return { error: "userNotFound" }; + } + if (sanitizedData.password.length < 6) { + console.log('X') + console.log(`LoginService :> Invalid password (${sanitizedData.username})`); + return { error: "invalidPassword" }; + } + const isPasswordValid = await Argon2id.verify( + Buffer.from(dbUser.passwordHash), + Buffer.from(sanitizedData.password), + { + secret: Buffer.from(`${process.env.HASH_SECRET}`), + algorithm: 2 + }); + if (!isPasswordValid) { + console.log(isPasswordValid) + console.log(`LoginService :> Invalid password (${sanitizedData.username})`); + return { error: "invalidPassword" }; + } + // biome-ignore lint/style/useConst: + let userData = { + error: "none", + jwt: null, + user: { + id: dbUser.id, + username: dbUser.username, + displayName: dbUser.displayName, + } + }; + + const alg = 'HS512'; + userData.jwt = await JwtSign({sub: dbUser.id}, alg, '1d', '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 + } +} + +const UserService = { + register: RegisterService, + login: LoginService, + getAll: getAllUsersService, + getFromId: getUserFromIdService, + edit: editUserService, + delete: deleteUserService +} + +export default UserService; \ No newline at end of file