diff --git a/.env.exemple b/.env.exemple new file mode 100644 index 0000000..2ed0415 --- /dev/null +++ b/.env.exemple @@ -0,0 +1,2 @@ +HASH_SECRET='' +JWT_SECRET='' \ No newline at end of file diff --git a/.gitignore b/.gitignore index ceaea36..efeef5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,132 +1,5 @@ -# ---> Node -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files +node_modules .env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - +.idea +pnpm-lock.yaml +yarn.lock \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..131fd90 --- /dev/null +++ b/app.js @@ -0,0 +1,26 @@ +const express = require('express') +const cors = require('cors') +const app = express() +const { Logger } = require('tslog'); + +const logger = new Logger({ name: "App" }); + +app.use(express.urlencoded({ extended: true })) +app.use(express.json()) +app.use(cors()) + +// ROUTES + +const AuthRoutes = require("./controllers/routes/auth") +const ImageRoutes = require("./controllers/routes/thread") +const EventRoutes = require("./controllers/routes/event") + +app.use('/auth', AuthRoutes) +app.use('/image', ImageRoutes) +app.use('/event', EventRoutes) + +app.listen(3333) +logger.info('Server is running !') +app.on('APP_STARTED', ()=>{ + +}) diff --git a/arkit.svg b/arkit.svg new file mode 100644 index 0000000..fe4e3d9 --- /dev/null +++ b/arkit.svg @@ -0,0 +1,163 @@ +Dependencies@node-rscorsexpressjosemongodbtsloguuidappauthAuthControllerAuthorizationMiddlewareCredentialServiceeventEventEventControllerEventServiceJwtServiceloggingMongodbServicethreadThreadControllerThreadServiceUserUserService \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4282cf8 --- /dev/null +++ b/biome.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "performance": { + "recommended": true + } + } + } +} diff --git a/controllers/AuthController.js b/controllers/AuthController.js new file mode 100644 index 0000000..1cba7a2 --- /dev/null +++ b/controllers/AuthController.js @@ -0,0 +1,343 @@ +const { + RegisterService, + LoginService, + getAllUsersService, + getUserFromIdService, + editUserService, + deleteUserService +} = require("../services/UserService"); +const {JwtVerify} = require("../services/JwtService"); +const { Logger } = require('tslog'); + +const logger = new Logger({ name: "Auth Controller" }); + +/** + * Registers a user. + * + * @async + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @return {Object} The response object containing the result of the registration. + * If successful, it will contain the registered user's data. + * If there is an error, it will contain an error name and message. + */ +async function registerUser(req, res) { + const body = req.body; + if (!body) { + logger.warn(`Invalid input data (${req.ip})`); + return res + .type('application/json') + .status(400) + .json({ error: 'Invalid input data' }); + } + if (!body.password || !body.username || !body.firstName || !body.lastName || !body.displayName) { + logger.warn(`Field(s) missing (${req.ip})`); + return res + .type('application/json') + .status(400) + .json({ error: 'Field(s) missing' }); + } + // sanitize data + let gdpr = false + if (body.gdpr === true) {gdpr = true} + const sanitizeData= { + username: `${body.username}`, + displayName: `${body.displayName}`, + gdpr: gdpr, + password: `${body.password}`, + firstName: `${body.firstName}`, + lastName: `${body.lastName}`, + }; + + /** + * Represents the result of the registration service. + * + * @typedef {Object} RegisterServiceResult + * @property {Promise} promise - The promise that resolves when the service registration is complete. + */ + const RegisterServiceResult = await RegisterService(sanitizeData) + + if (RegisterServiceResult.error === "gdprNotApproved") { + logger.warn(`GDPR not approved (${req.ip})`); + return res + .status(400) + .json({ + error: RegisterServiceResult.error, + message: "GDPR not accepted." + }); + } + if (RegisterServiceResult.error === "exist") { + logger.warn(`The user already exists (${req.ip})`); + return res + .type('application/json') + .status(400) + .json({ + error: RegisterServiceResult.error, + message: "The user already exists." + }); + } + + // SUCCESS + logger.info(`User registered successfully (${req.ip})`); + return res + .type('application/json') + .status(201) + .json(RegisterServiceResult); +} + +/** + * Logs in a user with the provided username and password. + * + * @param {Object} req - The request object containing the body data. + * @param {Object} res - The response object to send the result. + * @returns {Promise} - A promise that resolves once the login process is complete. + */ +async function loginUser(req, res) { + + const body = req.body; + if (!body) { + return res + .type('application/json') + .status(400) + .json({ error: 'Invalid input data' }); + } + if (!body.password || !body.username) { + logger.warn(`Field(s) missing (${req.ip})`); + return res + .type('application/json') + .status(400) + .json({ error: 'Field(s) missing' }); + } + + const loginData = { + username: `${body.username}`, + password: `${body.password}` + }; + console.log(body) + const LoginServiceResult = await LoginService(loginData); + console.log(LoginServiceResult) + + if (LoginServiceResult.error === "userNotFound") { + console.log('POOL') + return res + .type('application/json') + .status(404) + .json({ + error: LoginServiceResult.error, + message: "User not found." + }); + } + if (LoginServiceResult.error === "invalidPassword") { + return res + .type('application/json') + .status(401) + .json({ + error: LoginServiceResult.error, + message: "Invalid password." + }); + } + return res + .type('application/json') + .status(200) + .json(LoginServiceResult); +} + +//TODO - To test +async function getAllUsers(req, res) { + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) + if (!sourceUser) { + return res + .type('application/json') + .status(404) + .json({ error: 'You dont exist anymore' }); + } + if (!sourceUser.isAdmin) { + return res + .type('application/json') + .status(403) + .json({ error: 'Unauthorized' }); + } + const AllUserResponse = await getAllUsersService() + if (!AllUserResponse.users) { + return res + .type('application/json') + .status(500) + .json({ error: 'Internal server error' }); + } + return res + .type('application/json') + .status(200) + .json(AllUserResponse); +} + +//TODO - To test +/** + * Get user from the database based on the provided user ID and return it as a response. + * + * @async + * @param {object} req - The request object containing the user ID as a parameter. + * @param {object} res - The response object to be used for sending the user data or error. + * @return {Promise} - A Promise that resolves when the user data is sent to the client or an error occurred. + */ +async function getUser(req, res) { + const userId = req.params.userId; + const dbUser = await getUserFromIdService(userId); + if (!dbUser) { + logger.warn(`User not found (${req.ip})`); + return res + .type('application/json') + .status(404) + .json({ error: 'User not found' }); + } + return res + .type('application/json') + .status(200) + .json(dbUser); +} + +//TODO - To test +async function editUser(req, res) { + const body = req.body; + if (!body) { + return res + .type('application/json') + .status(400) + .json({ error: 'Field(s) missing' }); + } + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) + + /** + * Represents the user ID that is the target for a specific operation. + * + * @type {string|number} + */ + const targetUserId = body.targetId | payload.sub + + if (!sourceUser) { + logger.warn(`Unauthorized access attempt (${req.ip})`); + return res + .type('application/json') + .status(404) + .json({ error: 'You dont exist anymore' }); + } + if (sourceUser.isAdmin || sourceUser.id === payload.sub) { + if (sourceUser.isAdmin) { + logger.info(`EDIT :> Source user is an admin (${sourceUser.displayName})`) + } else { + logger.info(`EDIT :> Source user modify itself (${sourceUser.displayName})`) + } + + // biome-ignore lint/style/useConst: + let modifiedData= {} + if (body.firstName) modifiedData.firstName = `${body.firstName}`; + if (body.lastName) modifiedData.lastName = `${body.lastName}`; + if (body.displayName) modifiedData.displayName = `${body.displayName}`; + if (body.password) modifiedData.password = `${body.password}`; + + //Call service + const EditUserServiceResult = await editUserService(`${targetUserId}`, modifiedData); + if (EditUserServiceResult.error === 'userNotFound') { + logger.warn(`User not found (${req.ip})`); + return res + .type('application/json') + .status(404) + .json({ error: 'User not found' }); + } + if (EditUserServiceResult.error !== 'none') { + logger.error(`Error occurred during user edit (${req.ip})`); + return res + .type('application/json') + .status(500) + .json({ error: 'Internal server error' }); + } + return res + .type('application/json') + .status(200) + .json(EditUserServiceResult); + } + //Not itself or + logger.warn(`Unauthorized access attempt, not self or admin (${req.ip})`); + return res + .type('application/json') + .status(403) + .json({ error: 'Unauthorized' }); + + + } + +//TODO - To test +async function deleteUser(req, res) { + const body = req.body; + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) + const targetUserId = body.targetId | payload.sub + if (!sourceUser) { + logger.warn(`Unauthorized access attempt (${req.ip})`); + return res + .type('application/json') + .status(404) + .json({ error: 'You dont exist anymore' }); + } + if (sourceUser.isAdmin || sourceUser.id === payload.sub) { + const deleteUserServiceResult = await deleteUserService(`${targetUserId}`); + if (!deleteUserServiceResult) { + logger.error(`Error occurred during user delete (${req.ip})`); + return res + .type('application/json') + .status(500) + .json({ error: 'Internal server error' }); + } + + return res + .type('application/json') + .status(200) + .json({ message: 'User deleted successfully' }); + } +} + +/** + * Retrieves the user's information. + * + * @param {object} req - The incoming request object. + * @param {object} res - The outgoing response object. + */ +async function getSelf(req, res) { + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + console.log(payload) + const dbUser = await getUserFromIdService(payload.sub) + if (!dbUser) { + return res + .type('application/json') + .status(404) + .json({ error: 'User not found' }); + } + return res + .type('application/json') + .status(200) + .json({ + username: dbUser.username, + displayName: dbUser.displayName, + firstName: dbUser.firstName, + lastName: dbUser.lastName + }); +} + +module.exports = { + registerUser, + loginUser, + getAllUsers, + getUser, + editUser, + deleteUser, + getSelf +} \ No newline at end of file diff --git a/controllers/EventController.js b/controllers/EventController.js new file mode 100644 index 0000000..703d006 --- /dev/null +++ b/controllers/EventController.js @@ -0,0 +1,180 @@ +const { + getUserFromIdService +} = require("../services/UserService"); + +const { + JwtVerify +} = require("../services/JwtService"); + +const { + getAllEventsService, + getEventFromIdService, alterUserSubscribedEventStateService, getUserSubscribedEventService +} = require("../services/EventService"); + +//TODO - To test +/** + * Retrieves all events. + * + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @return {Object} The response object containing all events or an error message if events not found. + */ +async function getAllEvents(req, res) { + const events = await getAllEventsService('public'); + if (!events) { + return res.status(404).json({ message: "Events not found" }); + } + return res.status(200).json(events); +} + +//TODO - To test +/** + * Retrieves an event by ID. + * + * @param {object} req - The request object. + * @param {object} res - The response object. + * + * @return {object} Returns a response with the retrieved event. + * + * @throws {Error} Throws an error if the event ID is missing or if the event is not found. + */ +async function getEvent(req, res) { + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) + const targetId = req.params.id; + if (!targetId) { + return res.status(400).json({ message: "Event ID is missing" }); + } + const result = await getEventFromIdService(targetId) + if (!result) { + return res.status(404).json({ message: "Event not found" }); + } + return res.status(200).json(result); +} + +//TODO Owner user, admin user === +async function editEvent(req, res) { + const body = req.body; + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) + const targetId = body.targetId || sourceUser.id; + const eventTargetId = req.params.id + if (targetId !== sourceUser.id && !sourceUser.isAdmin) { + res.status(403).json({ message: "Unauthorized request" }); + } + if (!eventTargetId) { + res.status(400).json({ message: "Event target ID is missing" }); + } + +} + +//TODO Owner user, admin user === +async function deleteEvent(req, res) { + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) +} + +//TODO Event creation by logged user === +async function createNewEvent(req, res) { + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) +} + +//TODO - To test +/** + * Retrieves the subscribed event for the specified user. + * + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @returns {Object} The subscribed event information. + */ +async function getSubscribedEvent(req, res) { + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) + const targetId = body.targetId || sourceUser.id; + if (targetId !== sourceUser.id && !sourceUser.isAdmin) { + res.status(403).json({ message: "Unauthorized request" }); + } + const subscribedEventResult = await getUserSubscribedEventService(targetId); + if (subscribedEventResult.error === 'noSubscribedEventFound') { + return res + .type('application/json') + .status(404) + .json({ + error: 'noSubscribedEventFound', + message: 'No subscribed event found' + }); + } + return res + .type('application/json') + .status(200) + .json(subscribedEventResult); +} + +//TODO - To test +/** + * Alter the subscription state of an event for a user. + * @param {Object} req + * - The request object. + * @param {Object} req.body + * - The body of the request containing the desired subscription state. + * @param {string} req.headers.authorization + * - The authorization header containing the bearer token. + * @param {string} req.params.id + * - The ID of the target event. + * @param {Object} res + * - The response object. + * @returns {Object} The response object. + */ +async function alterSubscribedEventState(req, res) { + const body = req.body + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const sourceUser = await getUserFromIdService(payload.sub) + const eventTargetId = req.params.id; + const userTargetId = body.userId || sourceUser.id + const wantedState = body.subscribed === true + if (!eventTargetId) { + return res.status(400).json({ message: "Event target ID is missing" }); + } + if (userTargetId !== sourceUser.id && !sourceUser.isAdmin) { + return res.status(403).json({ message: "Unauthorized request" }); + } + + const alterEventSubStateResult = await alterUserSubscribedEventStateService( + userTargetId, + eventTargetId, + wantedState + ) + + if (alterEventSubStateResult.error === 'none') { + return res.status(200).json({ + message: "Event subscription state altered" + }); + } + return res.status(400).json({ + error: alterEventSubStateResult.error, + message: "Event subscription state not altered" + }); +} + +module.exports = { + getAllEvent: getAllEvents, + getEvent, + editEvent, + deleteEvent, + createNewEvent, + getSubscribedEvent, + alterSubscribedEventState +} \ No newline at end of file diff --git a/controllers/ImageController.js b/controllers/ImageController.js new file mode 100644 index 0000000..b4c1365 --- /dev/null +++ b/controllers/ImageController.js @@ -0,0 +1,22 @@ +//TODO Logged user, size limit, format type +async function addNewImage(req, res) { +} + +//TODO Admin user +async function GetAllImages(req, res) { +} + +//TODO Logged and non logged +async function getImage(req, res) { +} + +//TODO Owner user and admin user +async function deleteImage(req, res) { +} + +module.exports = { + addNewImage, + GetAllImages, + getImage, + deleteImage +} \ No newline at end of file diff --git a/controllers/ThreadController.js b/controllers/ThreadController.js new file mode 100644 index 0000000..ffd6ea2 --- /dev/null +++ b/controllers/ThreadController.js @@ -0,0 +1,281 @@ +const {JwtVerify} = require("../services/JwtService"); +const { + CreateThreadService, + GetAllThreadService, + GetThreadByIdService, + UpdateThreadService, + DeleteThreadService, + GetUserThreadService +} = require("../services/ThreadService"); +const req = require("express/lib/request"); + +/** + * Retrieves the payload of a JWT token from the request headers. + * @param {Object} req - The HTTP request object. + * @return {Promise} - The payload of the JWT token if it exists, otherwise false. + */ +async function getJwtPayload(req) { + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const jwtPayload = await JwtVerify(bearerToken); + if (jwtPayload) { + return jwtPayload; + } + console.log(`AUTH :> Invalid jwt (${req.ip})`) + return false +} + +/** + * CreateThreadController - Controller function for creating a thread + * + * @param {Object} req - The request object + * @param {Object} res - The response object + * + * @return {Promise} - A Promise that resolves to the response object with appropriate status and JSON payload + */ +async function CreateThreadController(req, res) { + console.log(`CTRL :> Thread creation (${req.ip})`) + const payload = await getJwtPayload(req) + if (payload) { + const body = req.body; + if (!body) { + return res + .type('application/json') + .status(400) + .json({ error: 'Invalid input data' }); + } + if (!body.title || !body.subTitle || !body.base64Banner || !body.desc || !body.price) { + return res + .type('application/json') + .status(400) + .json({ error: 'Field(s) missing' }); + } + const userId = payload.sub + const sanitizedData = { + userId: `${userId}`, + title: `${body.title}`, + subTitle: `${body.subTitle}`, + base64Banner: `${body.base64Banner}`, + desc: `${body.desc}`, + price: Number.parseFloat(body.price) + } + const CreateThreadServiceResult = await CreateThreadService(sanitizedData) + + if (!CreateThreadServiceResult) { + return res + .type('application/json') + .status(500) + .json({ error: 'Failed to create thread' }); + } + return res + .type('application/json') + .status(200) + .json({ success: 'Thread created successfully' }); + } +} + +/** + * Retrieves a thread by its ID. + * @async + * @function GetThreadByIdController + * @param {Object} req - The request object containing the parameters and body. + * @param {Object} res - The response object used to send the response. + * @return {Promise} - A promise that resolves once the thread is retrieved and the response is sent. + */ +async function GetThreadByIdController(req, res) { + const payload = await getJwtPayload(req) + const body = req.body; + if (payload) { + if (!req.params.id) { + return res + .type('application/json') + .status(400) + .json({ error: 'Field(s) missing' }); + } + const threadId = req.params.id; + const GetThreadByIdServiceResult = await GetThreadByIdService(threadId); + if (GetThreadByIdServiceResult.error === 'ThreadNotFound') { + return res + .type('application/json') + .status(404) + .json({ message: `${GetThreadByIdServiceResult.message}` }); + } + return res + .type('application/json') + .status(200) + .json(GetThreadByIdServiceResult); + } +} + +/** + * Retrieve all threads for a user. + * + * @param {Object} req - The request object. + * @param {Object} req.ip - The IP address of the requesting client. + * @param {Object} res - The response object. + * @returns {Promise} A promise that resolves with the JSON response containing all threads, or rejects with an error. + */ +async function GetAllThreadController(req, res) { + console.log(`CTRL :> Query all threads (${req.ip})`) + const payload = await getJwtPayload(req) + if (payload) { + const ServiceResult = await GetAllThreadService({userId: payload.sub}) + if (!ServiceResult) { + return res + .type('application/json') + .status(200) + .json({ error: 'No threads found' }); + } + if (ServiceResult.error) { + return res + .type('application/json') + .status(500) + .json({ error: ServiceResult.message }); + } + return res + .type('application/json') + .status(200) + .json(ServiceResult); + } +} + +/** + * Retrieves a user's thread from the server. + * + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @return {Object} The response object containing the retrieved user thread. + * @throws {Error} If an error occurs while retrieving the user thread. + */ +async function GetUserThreadController(req, res) { + const payload = await getJwtPayload(req) + const GetUserThreadServiceResult = await GetUserThreadService(payload.sub); + if (GetUserThreadServiceResult.error === 'ThreadNotFound') { + return res + .type('application/json') + .status(200) + .json({ error: 'No threads found' }); + } + if (GetUserThreadServiceResult.error) { + return res + .type('application/json') + .status(500) + .json({ error: GetUserThreadServiceResult.message }); + } + return res + .type('application/json') + .status(200) + .json(GetUserThreadServiceResult); +} + +/** + * UpdateThreadController is an asynchronous function that handles the logic for updating a thread. + * + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @return {Object} - The response object. + */ +async function UpdateThreadController(req, res) { + const payload = await getJwtPayload(req) + if (payload) { + const body = req.body; + if (!body) { + return res + .type('application/json') + .status(400) + .json({ error: 'Invalid input data' }); + } + if (!req.params.id) { + return res + .type('application/json') + .status(400) + .json({ error: 'Missing identifier' }); + } + const threadId = req.params.id; + if (!body) { + return res + .type('application/json') + .status(400) + .json({ error: 'Invalid input data' }); + } + if (!body.title || !body.subTitle || !body.base64Banner || !body.desc || !body.price || !body.userId) { + return res + .type('application/json') + .status(400) + .json({ error: 'Field(s) missing' }); + } + const sanitizedData = { + title: `${body.title}`, + subTitle: `${body.subTitle}`, + base64Banner: `${body.base64Banner}`, + desc: `${body.desc}`, + price: Number.parseFloat(body.price) + } + const UpdateThreadServiceResult = await UpdateThreadService(threadId, sanitizedData); + + if (UpdateThreadServiceResult.error === 'ThreadNotFound') { + return res + .type('application/json') + .status(404) + .json({ message: `${UpdateThreadServiceResult.message}` }); + } + return res + .type('application/json') + .status(200) + .json({ message: 'Thread updated successfully' }); + } +} + +/** + * Deletes a thread. + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @return {Promise} - A promise that resolves with the response. + */ +async function DeleteThreadController(req, res) { + const payload = await getJwtPayload(req) + if (payload) { + const body = req.body; + if (!body) { + return res + .type('application/json') + .status(400) + .json({ error: 'Invalid input data' }); + } + if (!req.params.id) { + return res + .type('application/json') + .status(400) + .json({ error: 'Field(s) missing' }); + } + + const threadId = `${body.id}`; + const DeleteThreadServiceResult = await DeleteThreadService(threadId); + + if (!DeleteThreadServiceResult) { + return res + .type('application/json') + .status(500) + .json({ error: 'Failed to delete thread' }); + } + if (DeleteThreadServiceResult.error === 'ThreadNotFound') { + return res + .type('application/json') + .status(404) + .json({ message: `${DeleteThreadServiceResult.message}` }); + } + return res + .type('application/json') + .status(200) + .json({ message: 'Thread deleted successfully' }); + } +} + +module.exports = { + CreateThreadController, + GetThreadByIdController, + GetAllThreadController, + GetUserThreadController, + UpdateThreadController, + DeleteThreadController +} \ No newline at end of file diff --git a/controllers/routes/auth/index.js b/controllers/routes/auth/index.js new file mode 100644 index 0000000..4d68da0 --- /dev/null +++ b/controllers/routes/auth/index.js @@ -0,0 +1,23 @@ +const express = require("express"); +const router = express.Router() + +const { + getSelf, + loginUser, + registerUser, + getUser, + editUser, + deleteUser +} = require("../../AuthController"); +const {validateJWT} = require("../../../middlewares/AuthorizationMiddleware"); + +router.route("/login").post(loginUser) +router.route("/register").post(registerUser) + +router.route("/me").get(validateJWT, getSelf) + +router.route("/:id").get(validateJWT, getUser) +router.route("/:id").patch(validateJWT, editUser) +router.route("/:id").delete(validateJWT, deleteUser) + +module.exports = router diff --git a/controllers/routes/event/index.js b/controllers/routes/event/index.js new file mode 100644 index 0000000..e06de88 --- /dev/null +++ b/controllers/routes/event/index.js @@ -0,0 +1,24 @@ +const express = require("express"); +const router = express.Router() + +const { + getSubscribedEvent, + getEvent, + getAllEvent, + alterSubscribedEventState, + createNewEvent, + deleteEvent, + editEvent +} = require("../../EventController"); +const {validateJWT} = require("../../../middlewares/AuthorizationMiddleware"); + +router.route("/all").get(getAllEvent) +router.route("/subscribed").get(validateJWT, getSubscribedEvent) +router.route("/subscribed").post(validateJWT, alterSubscribedEventState) +router.route("/new").post(validateJWT, createNewEvent) + +router.route("/:id").get(validateJWT, getEvent) +router.route("/:id").patch(validateJWT, editEvent) +router.route("/:id").delete(validateJWT, deleteEvent) + +module.exports = router diff --git a/controllers/routes/image/index.js b/controllers/routes/image/index.js new file mode 100644 index 0000000..279ced7 --- /dev/null +++ b/controllers/routes/image/index.js @@ -0,0 +1,13 @@ +const express = require("express"); +const router = express.Router() + +const {} = require("../../EventController"); +const {isAdmin, validateJWT} = require("../../../middlewares/AuthorizationMiddleware"); + +router.route("/all").get(isAdmin) +router.route("/new").post(validateJWT) + +router.route("/:id").get(validateJWT) +router.route("/:id").delete(validateJWT) + +module.exports = router diff --git a/controllers/routes/thread/index.js b/controllers/routes/thread/index.js new file mode 100644 index 0000000..1623e04 --- /dev/null +++ b/controllers/routes/thread/index.js @@ -0,0 +1,28 @@ +const express = require("express"); +const { + CreateThreadController, + GetThreadByIdController, + GetAllThreadController, + UpdateThreadController, + DeleteThreadController, + GetUserThreadController +} = require("../../ThreadController"); +const {validateJWT} = require("../../../middlewares/AuthorizationMiddleware"); + +const router = express.Router() + +// CREATE +router.route("/new").post(validateJWT, CreateThreadController) + +// READ +router.route("/all").get(validateJWT, GetAllThreadController) +router.route("/user").get(validateJWT, GetUserThreadController) +router.route("/:id").get(validateJWT, GetThreadByIdController) + +// UPDATE +router.route("/:id").patch(validateJWT, UpdateThreadController) + +// DELETE +router.route("/:id").delete(validateJWT, DeleteThreadController) + +module.exports = router \ No newline at end of file diff --git a/middlewares/AuthorizationMiddleware.js b/middlewares/AuthorizationMiddleware.js new file mode 100644 index 0000000..9420cd7 --- /dev/null +++ b/middlewares/AuthorizationMiddleware.js @@ -0,0 +1,49 @@ +const {JwtVerify} = require("../services/JwtService"); +const {getUserFromId} = require("../services/UserService"); +const {log} = require("../utils/logging"); +const UNAUTHORIZED = 401; +const FORBIDDEN = 403; +const UNAUTH_MESSAGE = 'Missing Authorization Header'; +const INVALID_TOKEN_MESSAGE = 'Invalid or expired token.'; + +async function validateJWT(req, res, next) { + log('MIDDLEWARE', 'JWT', `Vérification du jwt... (${req.ip})`) + const authHeader = req.headers.authorization; + if (!authHeader) { + res.status(UNAUTHORIZED).json({message: UNAUTH_MESSAGE}); + return; + } + + const bearerToken = authHeader.split(' ')[1]; + const isTokenValid = await JwtVerify(bearerToken); + + if (isTokenValid !== false) { + log('MIDDLEWARE', 'JWT', `Token valide. (${req.ip})`) + next(); + } else { + log('MIDDLEWARE', 'CHECK', `Token invalide (${req.ip})`) + res.status(FORBIDDEN).json({message: INVALID_TOKEN_MESSAGE}); + } +} + +async function isAdmin(req, res, next) { + log('MIDDLEWARE', 'ROLE', `Vérification du role... (${req.ip})`) + const authHeader = req.headers.authorization; + const bearerToken = authHeader.split(' ')[1]; + const payload = await JwtVerify(bearerToken); + const dbUser = await getUserFromId(payload.sub) + if (!dbUser || !dbUser.isAdmin) { + log('MIDDLEWARE', 'ROLE', `Non admin ou éxistant. (${req.ip})`) + return res + .type('application/json') + .status(403) + .json({ error: 'Unauthorized' }); + } + log('MIDDLEWARE', 'ROLE', `Accès admin validé. (${req.ip})`) + next(); +} + +module.exports = { + validateJWT, + isAdmin +}; \ No newline at end of file diff --git a/models/Event.js b/models/Event.js new file mode 100644 index 0000000..ff4be5f --- /dev/null +++ b/models/Event.js @@ -0,0 +1,37 @@ +const { v4: uuid, parse } = require('uuid'); + +class Event { + members = []; + constructor(title, subTitle, base64Banner, desc, date, were, maxMembers, authorId, id, members) { + if (!id || parse(id)) { + this.id = uuid(undefined, undefined, undefined); + } else { + this.id = id; + } + if (members) { + this.members = members; + } + this.title = title; + this.subTitle = subTitle; + this.base64Banner = base64Banner; + this.desc = desc; + this.date = date; + this.were = were; + this.maxMembers = maxMembers; + this.authorId = authorId; + this.members.push(`${authorId}`) + } + addMember(memberId) { + this.members.push(`${memberId}`); + } + getMembers() { + return this.members; + } + getMemberCount() { + return this.members.length; + } + removeMember(id) { + this.members = this.members.filter(member => member !== id); + } +} +module.exports = Event; \ No newline at end of file diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..0ebe529 --- /dev/null +++ b/models/User.js @@ -0,0 +1,44 @@ +const { v4: uuid, parse } = require('uuid'); + +/** + * Represents a User object. + * + * @class User + */ +class User { + firstName; + lastName; + /** + * Creates a new user object. + * + * @constructor + * @param {string} username - The username of the user. + * @param {string} displayName - The display name of the user. + * @param {string} passwordHash - The password hash of the user. + * @param {string} [id] - The optional unique identifier of the user. If not provided or not a valid UUID, a new UUID will be generated. + * @return {void} + */ + constructor(username, displayName, passwordHash, gdpr, id) { + if (!id || parse(id)) { + this.id = uuid(undefined, undefined, undefined); + } else { + this.id = id; + } + console.log(this.id) + this.username = username; + this.displayName = displayName; + this.gdpr = gdpr; + this.passwordHash = passwordHash; + this.isAdmin = false; + this.isDisabled = false + } + + setFirstName(firstName) { + this.firstName = firstName + } + + setLastName(lastName) { + this.lastName = lastName + } +} +module.exports = User; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..60c3f38 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "brief-04-back", + "version": "1.0.0", + "description": "", + "main": "app.js", + "scripts": { + "dev": "nodemon --enable-source-maps", + "schema": "arkit -f app.js" + }, + "author": "Mathis HERRIOT", + "license": "MIT", + "dependencies": { + "@node-rs/argon2": "^1.8.0", + "cors": "^2.8.5", + "express": "^4.19.2", + "jose": "^5.2.3", + "mongodb": "^6.5.0", + "tslog": "^4.9.2", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@biomejs/biome": "^1.6.4", + "arkit": "^1.6.4", + "nodemon": "^3.1.0" + } +} diff --git a/services/CredentialService.js b/services/CredentialService.js new file mode 100644 index 0000000..59c199a --- /dev/null +++ b/services/CredentialService.js @@ -0,0 +1,18 @@ +const Argon2id = require("@node-rs/argon2"); + +/** + * Generates a hash from a given password using Argon2id algorithm. + * + * @param {string} password - The password to generate a hash for. + * @return {Promise} - The generated hash. + */ +async function getHashFromPassword(password) { + return await Argon2id.hash(password,{ + secret: Buffer.from(`${process.env.HASH_SECRET}`), + algorithm: 2 + }) +} + +module.exports = { + getHashFromPassword +} \ No newline at end of file diff --git a/services/EventService.js b/services/EventService.js new file mode 100644 index 0000000..5807acc --- /dev/null +++ b/services/EventService.js @@ -0,0 +1,193 @@ +const {getDatabase} = require("./MongodbService"); +let Db = null +getDatabase("brief04").then((value)=>{Db = value}) + +const { Logger } = require('tslog'); + +const logger = new Logger({ name: "Auth Controller" }); + +//TODO Better return error integration === +/** + * Retrieves an event from the database using the specified ID. + * @param {string} id - The ID of the event to retrieve. + * @return {Promise} - A Promise that resolves to an object representing the event. + */ +async function getEventFromIdService(id) { + return await Db.collection("events").findOne({id: id}); +} + +//TODO - To test +/** + * Retrieves all events from the database. + * + * @param {string} sourceId - The source ID to query the events from. + * @return {object} - An object containing: + * - updatedAt : The timestamp when the events were last updated. + * - events : An array of sanitized event objects. Each event object contains the following properties: + * - id : The event ID. + * - title : The event title. + * - subTitle : The event subtitle. + * - base64Banner : The base64 encoded banner image of the event. + * - desc : The event description. + * - date : The event date. + * - were : The event location. + * - maxMembers : The maximum number of members allowed for the event. + * - authorId : The ID of the event author. + * - members : The number of members currently participating in the event. + * - length : The number of events returned. + */ +async function getAllEventsService(sourceId) { + logger.info(`EVENT :> Query all threads (${sourceId})`) + let eventsArray = [] + eventsArray = await Db.collection("events").find().toArray(); + if (!eventsArray) { + logger.error('No event found.') + return null + } + const sanitizedEvents = eventsArray.map((event)=>{ + return { + id: event.id, + title: event.title, + subTitle: event.subTitle, + base64Banner: event.base64Banner, + desc: event.desc, + date: event.date, + were: event.were, + maxMembers: event.maxMembers, + authorId: event.authorId, + members: event.members + } + }) + console.log(` -> Returned ${sanitizedEvents.length} event(s)`) + return { + updatedAt: Date.now(), + events: sanitizedEvents, + length: sanitizedEvents.length + } +} + +//TODO - To test +/** + * Retrieves the subscribed event(s) for a given user. + * + * @param {string} targetId - The id of the target user. + * + * @return {Promise} + * - A promise that resolves to an object containing information about the + * subscribed event(s). + * - If there are subscribed event(s), the resolved object will include the + * following properties: + * - iat: The current timestamp in milliseconds. + * - subscribedEvent: An array of the subscribed event(s) retrieved from the database. + * - length: The number of subscribed event(s). + * - If no subscribed event is found for the given targetId, the resolved object will + * include the following error property: + * - error: "noSubscribedEventFound" + */ +async function getUserSubscribedEventService(targetId) { + const subscribedEvent = await Db.collection("events").find({ + members: { + $eltMatch: { + $eq: {targetId} + }}}).toArray(); + if (!subscribedEvent) { + logger.error(`No subscribed event found for USERID:${targetId}`) + return { + error: "noSubscribedEventFound" + } + } + return { + iat: Date.now(), + subscribedEvent: subscribedEvent, + length: subscribedEvent.length + } +} + +//TODO - To test +/** + * Modify the subscription state of a user for an event. + * + * @param {string} userId - The ID of the user. + * @param {string} eventId - The ID of the event. + * @param {boolean} state - The desired subscription state (true for subscribed, false for unsubscribed). + * + * @return {object} - The result of the operation. + * - If the user is not found, { error: "userNotFound" } is returned. + * - If the event is not found, { error: "eventNotFound" } is returned. + * - If the user is already subscribed to the event and the state is true, { error: "alreadySubscribed" } is returned. + * - If the user is not subscribed to the event and the state is false, { error: "notSubscribed" } is returned. + * - If the event update fails, { error: "updateFailed" } is returned. + * - If the operation is successful, { error: "none" } is returned. + */ +async function alterUserSubscribedEventStateService(userId, eventId, state){ + const user = await Db.collection("users").findOne({id: userId}); + if (!user) { + logger.error(`User not found (${userId})`); + return { error: "userNotFound" }; + } + const event = await Db.collection("events").findOne({id: eventId}); + if (!event) { + logger.error(`Event not found (${eventId})`); + return { error: "eventNotFound" }; + } + const isUserSubscribed = event.members.includes(userId); + if (state === true && isUserSubscribed) { + logger.error(`User already subscribed to the event (${userId}, ${eventId})`); + return { error: "alreadySubscribed" }; + } + if (state === false && !isUserSubscribed) { + logger.error(`User is not subscribed to the event (${userId}, ${eventId})`); + return { error: "notSubscribed" }; + } + if (state === true) { + event.members.push(userId); + } else if (state === false) { + event.members = event.members.filter(memberId => memberId !== userId); + } + const updatedEventResult = await Db.collection("events").updateOne({id: eventId}, {$set: event}); + if (updatedEventResult.modifiedCount === 0) { + logger.error(`Failed to update event (${eventId})`); + return { error: "updateFailed" }; + } + logger.info(`User ${state === true ? "subscribed" : "unsubscribed"} successfully to the event. (${userId}, ${eventId})`); + return { error: "none" }; +} + +//TODO - To test +/** + * Edits an event in the database. + * + * @param {string} eventId + * - The ID of the event to be edited. + * @param {object} sanitizedData + * - The sanitized data used to update the event. + * + * @return {object} + * - An object with an "error" property. + * - If the event is not found, the "error" property will be "eventNotFound". + * - If the event is edited successfully, the "error" property will be "none". + */ +async function editEventService(eventId, sanitizedData) { + try { + const updatedEventResult = await Db.collection("events").updateOne({id: eventId}, {$set: sanitizedData}); + if (updatedEventResult.modifiedCount === 0) { + logger.info(`EDIT :> Event not found (${eventId})`) + return { error: "eventNotFound" }; + } + logger.info(`EDIT :> Event edited successfully (${eventId})`); + return { error: "none" }; + } catch (e) { + logger.error(e) + } + +} + +//TODO Delete event - Owner || Admin === + +module.exports = { + getEventFromIdService, + getAllEventsService, + getUserSubscribedEventService, + alterUserSubscribedEventStateService, + editEventService +} \ No newline at end of file diff --git a/services/JwtService.js b/services/JwtService.js new file mode 100644 index 0000000..ffca390 --- /dev/null +++ b/services/JwtService.js @@ -0,0 +1,47 @@ +const Jose = require("jose") +const { Logger } = require('tslog') + +const logger = new Logger({ name: "JwtService" }); + +/** + * Validates a JWT and returns the result. + * + * @param {string} jwt - The JWT to be validated. + * @return {Promise} A promise that resolves to the validation result. It returns `false` if the JWT is invalid, otherwise it returns the decoded payload as an object. + */ +async function JwtVerifyService(jwt) { + try { + const result = await Jose.jwtVerify( + jwt, + new TextEncoder() + .encode(`${process.env.JWT_SECRET}`), + { + }) + return result.payload; + } catch (error) { + logger.error(error) + return false + } +} + +/** + * Sign a JWT with the given payload, algorithm, expiration time, and audience. + * + * @param {object} payload - The payload of the JWT. + * @param {string} alg - The algorithm to use for signing the JWT. + * @param {string} expTime - The expiration time of the JWT in format "4h || 7d || 1w". + * @param {string|string[]} audience - The audience(s) of the JWT. + * + * @returns {Promise} - A promise that resolves to the signed JWT. + */ +async function JwtSignService(payload, alg, expTime, audience) { + return await new Jose.SignJWT(payload) + .setProtectedHeader({alg}) + .setIssuedAt(new Date()) + .setIssuer('Brief 03 - Mathis HERRIOT') + .setAudience(audience) + .setExpirationTime(expTime) + .sign(new TextEncoder().encode(`${process.env.JWT_SECRET}`)) +} + +module.exports = {JwtVerify: JwtVerifyService, JwtSign: JwtSignService} \ No newline at end of file diff --git a/services/MongodbService.js b/services/MongodbService.js new file mode 100644 index 0000000..fd44456 --- /dev/null +++ b/services/MongodbService.js @@ -0,0 +1,28 @@ +const {MongoClient} = require('mongodb') +const { Logger } = require('tslog') + +/** + * Establishes a connection to a MongoDB server using the MongoClient. + * + * @returns {MongoClient} A Promise that resolves with a MongoDB client object connected to the server. + * The client object can be used to perform database operations. + * @throws {Error} If an error occurs while attempting to connect to the MongoDB server. + */ +async function connect() { + try { + return await MongoClient.connect("mongodb://127.0.0.1:27017/") + } catch (err) { + throw new Error(err) + } +} + +async function getDatabase(name) { + const {connect} = require("./MongodbService") + const client = await connect() + //console.log(client) + const db = client.db(name); + return db; + +} + +module.exports = {connect, getDatabase} \ No newline at end of file diff --git a/services/ThreadService.js b/services/ThreadService.js new file mode 100644 index 0000000..47e7c83 --- /dev/null +++ b/services/ThreadService.js @@ -0,0 +1,197 @@ +const {getDatabase} = require("./MongodbService"); +const Thread = require("../models/Event"); +let Db = null +getDatabase("brief04").then((value)=>{Db = value}) + +async function getThreadFromId(id) { + return await Db.collection("threads").findOne({id: id}); +} + +/** + * Asynchronously creates a thread service. + * + * @param {Object} sanitizedData - The sanitized data object containing necessary information for creating the thread service. + * @param {string} sanitizedData.userId + * @param {string} sanitizedData.title + * @param {string} sanitizedData.subTitle + * @param {string} sanitizedData.base64Banner + * @param {string} sanitizedData.desc + * @param {number} sanitizedData.price + * + * @return {Promise} A Promise that resolves to the created thread service. + */ +async function CreateThreadService(sanitizedData) { + console.log(`SERV :> Create thread (${sanitizedData.title})`) + const NewThread = new Thread(sanitizedData.title, sanitizedData.subTitle, sanitizedData.base64Banner, sanitizedData.desc, sanitizedData.price, sanitizedData.userId) + const dbResult = await Db.collection("threads").insertOne(NewThread); + if (!dbResult.acknowledged) { + console.log(" -> FAIL") + return null + } + console.log(` -> ${NewThread.id} = Success`) + return NewThread.id; +} + +// TODO OK +/** + * Retrieve a thread by its ID. + * + * @param {string} threadId - The ID of the thread to retrieve. + * @return {Promise} The thread object if found, otherwise an error object. + * The error object has the following properties: + * - error: "ThreadNotFound" + * - message: "Thread not found" + */ +async function GetThreadByIdService(threadId) { + console.log(`SERV :> Get thread (${threadId})`) + const targetThread = await getThreadFromId(threadId); + if (!targetThread) { + console.log(` -> Thread not found (${threadId})`) + return { + error: "ThreadNotFound", + message: "Thread not found" + } + } + console.log(` -> Thread found (${threadId})`) + return { + id: targetThread.id, + title: targetThread.title, + subTitle: targetThread.subTitle, + base64Banner: targetThread.base64Banner, + desc: targetThread.desc, + price: targetThread.price, + userId: targetThread.userId + }; +} + +// TODO OK +/** + * Retrieves all threads for a given user. + * + * @param {object} sanitizedData - The sanitized data object containing user ID. + * + * @return {object} - An object containing the updated timestamp, an array of sanitized threads, and the length of the array. + * If the user is not found, an error object with the corresponding error code and message is returned. + */ +async function GetAllThreadService(sanitizedData) { + console.log(`SERV :> Query all threads (${sanitizedData.userId})`) + const sourceUser = Db.collection('users').findOne({id: sanitizedData.userId}) + if (!sourceUser) { + console.log(` -> User not found (${sanitizedData.userId})`) + return { + error: "UserNotFound", + message: "User not found" + } + } + let threadsArray = [] + threadsArray = await Db.collection("threads").find().toArray(); + const sanitizedThreads = threadsArray.map((thread)=>{ + return { + id: thread.id, + title: thread.title, + subTitle: thread.subTitle, + base64Banner: thread.base64Banner, + desc: thread.desc, + price: thread.price, + userId: thread.userId + } + }) + console.log(` -> Returned ${sanitizedThreads.length} thread(s)`) + return { + updatedAt: Date.now(), + threads: sanitizedThreads, + length: sanitizedThreads.length + } +} + +/** + * Retrieves the user thread(s) for the given userId. + * + * @param {string} userId - The ID of the user. + * @return {Promise} - A promise that resolves to an object containing the user threads, or an error object if no threads are found. + */ +async function GetUserThreadService(userId) { + console.log(`SERV :> Get user thread(s) (${userId})`) + const userThreads = await Db.collection("threads").find({ userId: userId }).toArray(); + if (!userThreads) { + console.log(` -> Thread not found (${userId})`) + return { + error: "ThreadNotFound", + message: "Thread not found" + } + } + console.log(` -> ${userThreads.length} thread(s) found.`) + const cleanUserThreads = userThreads.map((thread) => { + return { + id: thread.id, + title: thread.title, + subTitle: thread.subTitle, + base64Banner: thread.base64Banner, + desc: thread.desc, + price: thread.price, + userId: thread.userId + }; + }); + console.log(cleanUserThreads) + return { + iat: Date.now(), + threads : cleanUserThreads, + length: cleanUserThreads.length + }; +} + +/** + * Updates a thread in the database with the given threadId and sanitized data. + * + * @param {string} threadId - The unique identifier of the thread to update. + * @param {object} sanitizedData - The sanitized data to update the thread with. + * @return {object} - An object indicating the result of the update operation. + * If the thread is not found, an error object with the error type and message is returned. + * If the thread is found and successfully updated, an object with an "error" property set to "none" is returned. + */ +async function UpdateThreadService(threadId, sanitizedData) { + const updatedThread = await Db.collection("threads").findOneAndUpdate({ id: threadId }, { $set: sanitizedData }, { returnOriginal: false }); + console.log(updatedThread) + if (!updatedThread) { + console.log(` -> Thread not found (${threadId})`); + return { + error: "ThreadNotFound", + message: "Thread not found" + }; + } + console.log(` -> Thread updated (${threadId})`); + return { + error: "none" + }; +} + +/** + * Deletes a thread with the given thread ID from the database. + * + * @param {string} threadId - The ID of the thread to delete. + * @return {Promise<{ error: string } | { error: string, message: string }>} - A promise that resolves to an object with either an "error" property set to "none" if the thread was deleted successfully, or an "error" property set to "ThreadNotFound" with a "message" property set to "Thread not found" if the thread was not found. + */ +async function DeleteThreadService(threadId) { + console.log(`SERV :> Delete thread (${threadId})`); + const deletedThread = await Db.collection("threads").findOneAndDelete({ id: threadId }); + if (!deletedThread) { + console.log(` -> Thread not found (${threadId})`); + return { + error: "ThreadNotFound", + message: "Thread not found" + }; + } + console.log(` -> Thread deleted (${threadId})`); + return { + error: "none" + } +} + +module.exports = { + CreateThreadService, + GetThreadByIdService, + GetAllThreadService, + GetUserThreadService, + UpdateThreadService, + DeleteThreadService +} \ No newline at end of file diff --git a/services/UserService.js b/services/UserService.js new file mode 100644 index 0000000..78decb2 --- /dev/null +++ b/services/UserService.js @@ -0,0 +1,229 @@ +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(); + 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) { + 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 +} \ No newline at end of file diff --git a/types/requests/IReqLoginData.ts b/types/requests/IReqLoginData.ts new file mode 100644 index 0000000..e414ee3 --- /dev/null +++ b/types/requests/IReqLoginData.ts @@ -0,0 +1,4 @@ +interface IReqLoginData { + username: string; + password: string; +} \ No newline at end of file diff --git a/types/requests/IReqRegisterData.ts b/types/requests/IReqRegisterData.ts new file mode 100644 index 0000000..e9eb51b --- /dev/null +++ b/types/requests/IReqRegisterData.ts @@ -0,0 +1,8 @@ +interface IReqRegisterData { + username: string; + displayName: string; + password: string; + firstName: string; + lastName: string; + gdpr: boolean; +} \ No newline at end of file diff --git a/types/responses/IResLoginData.ts b/types/responses/IResLoginData.ts new file mode 100644 index 0000000..d172bd7 --- /dev/null +++ b/types/responses/IResLoginData.ts @@ -0,0 +1,11 @@ +interface IResLoginData { + error: string; + jwt: string; + user: User; +} + +interface User { + id: string; + username: string; + displayName: string; +} \ No newline at end of file diff --git a/types/responses/IResRegisterData.ts b/types/responses/IResRegisterData.ts new file mode 100644 index 0000000..97ba8a4 --- /dev/null +++ b/types/responses/IResRegisterData.ts @@ -0,0 +1,13 @@ +interface IResRegisterData { + error: string; + jwt: string; + user: User; +} + +interface User { + id: string; + username: string; + displayName: string; + firstName: string; + lastName: string; +} \ No newline at end of file diff --git a/utils/logging.js b/utils/logging.js new file mode 100644 index 0000000..de63f97 --- /dev/null +++ b/utils/logging.js @@ -0,0 +1,14 @@ +/** + * Logs a message with a specific category and state. + * + * @param {string} category - MIDDLEWARE | CONTROLLER | SERVICE + * @param {string} state - REQ | RES | ERR | WARN | INFO + * @param {string} message - The message to be logged. + * @return {undefined} - No return value. + */ +function newLog(category, state, message) { + console.log(`[${category}] ${state} :> \n ${message}`); +} +module.exports = { + log: newLog +} \ No newline at end of file