const Argon2id = require("@node-rs/argon2") const {getDatabase} = require("./MongodbService"); const {getHashFromPassword} = require("./CredentialService") const {JwtSign} = require("./JwtService"); const User = require("../models/User"); const { Logger } = require('tslog') const logger = new Logger({ name: "UserService" }); let Db = null getDatabase("brief04").then((value)=>{Db = value}) /** * Retrieves a user from the database based on the provided username. * * @param {string} username - The username of the user to retrieve. * * @return {Promise} - A promise that resolves with the user object if found, * or null if not found. */ async function getUserFromUsername(username) { const dbUser = await Db.collection("users").findOne({username: `${username}`}) if (dbUser === undefined) return null; return dbUser; } /** * Retrieves a user from the database based on the provided id. * * @param {string} id - The id of the user. * @returns {Promise} - A Promise that resolves with the user object if found, * or null if no user is found. */ async function getUserFromIdService(id) { return await Db.collection("users").findOne({id: id}); } /** * 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 } } module.exports = { RegisterService, LoginService, getAllUsersService, getUserFromIdService, editUserService, deleteUserService }