diff --git a/__tests__/user/__tests__/users.test.ts b/__tests__/user/__tests__/users.test.ts index 06c55b5..18bc4e3 100644 --- a/__tests__/user/__tests__/users.test.ts +++ b/__tests__/user/__tests__/users.test.ts @@ -1,11 +1,13 @@ -import { app } from '../../../src/app/main'; -import { Config } from "../../../src/app/config/config"; -import { loadDatabase } from "../../../src/app/db/index"; -import generateToken from '../../../src/common/utils/generateToken'; - +// External Modules import request from 'supertest'; import { describe, test, expect, beforeAll, jest, beforeEach } from 'bun:test'; +// Internal Modules +import generateToken from 'src/common/utils/generateToken'; +import { Config } from 'src/app/config/config'; +import { loadDatabase } from 'src/app/db'; +import { app } from 'src/app/main'; + const contentTypeKey = 'Content-Type'; const contentTypeValue = /json/; const httpSuccess = 200; @@ -46,7 +48,7 @@ describe('INTEGRATION - User Module', () => { { statusCode: httpUnauthorized, error: "Unauthorized", - message: "Invalid token" + message: "Unauthorized" } ); }) diff --git a/docker/ARM64.Dockerfile b/docker/ARM64.Dockerfile index b132177..1d42be7 100644 --- a/docker/ARM64.Dockerfile +++ b/docker/ARM64.Dockerfile @@ -22,9 +22,9 @@ FROM base AS release COPY --from=install /temp/prod/node_modules node_modules COPY --from=prerelease /bun/dist ./dist -RUN mkdir -p /bun/src/image/assets/images -RUN chown -R bun:bun /bun/src/image/assets/images -RUN chmod -R 600 /bun/src/image/assets/images +RUN mkdir -p /bun/src/image/assets/images && \ + chown -R bun:bun /bun/src/image/assets/images && \ + chmod -R 600 /bun/src/image/assets/images USER bun EXPOSE 4000 diff --git a/docker/Dockerfile b/docker/Dockerfile index 9e44e87..98fd8a8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,9 +22,9 @@ FROM base AS release COPY --from=install /temp/prod/node_modules node_modules COPY --from=prerelease /bun/dist ./dist -RUN mkdir -p /bun/src/image/assets/images -RUN chown -R bun:bun /bun/src/image/assets/images -RUN chmod -R 600 /bun/src/image/assets/images +RUN mkdir -p /bun/src/image/assets/images && \ + chown -R bun:bun /bun/src/image/assets/images && \ + chmod -R 600 /bun/src/image/assets/images USER bun EXPOSE 4000 diff --git a/src/image/image-controller.ts b/src/image/image-controller.ts index f96bc26..93559b9 100644 --- a/src/image/image-controller.ts +++ b/src/image/image-controller.ts @@ -1,52 +1,66 @@ // External Modules import { Request, Response } from 'express'; +import { LogArgument } from 'rollbar'; // Interal Modules -import clearTemporaryFiles from '../common/utils/clear'; +import clearTemporaryFiles from 'src/common/utils/clear'; import ImageService from './image-service'; -import { Query } from '../common/interfaces/types'; +import { Query } from 'src/common/interfaces/types'; +import { rollbar } from 'src/app/config/rollbar'; class ImageController { /** - * @description Upload a file to cloudinary then saves the url on mongodb - * @param { Request } req.body - tag, source, is_nsfw - * @param { Request } file.path - path to the file - * @returns { Promise } A success message with a Json response format + * @description Uploads a file to Cloudinary and saves the URL in MongoDB + * @param { Request } req - The request object containing the file and additional data + * @returns { Promise } A JSON response with a success message */ async uploadFile(req: Request, res: Response): Promise { try { const { file } = req; const { tag, source, is_nsfw } = req.body; - const response = await ImageService.cloudinaryUpload(file?.path); - const { public_id, secure_url } = response; - ImageService.upload({ + + if (!file) { + return res.status(400).json({ error: 'File is required' }); + } + + const uploadResult = await ImageService.cloudinaryUpload(file.path); + const { public_id, secure_url } = uploadResult; + + await ImageService.upload({ source, is_nsfw, id: public_id, url: secure_url, tag, }); - clearTemporaryFiles(file?.path ?? 'image/assets/images'); - return res.json({ url: 'Imagen guardada correctamente' }); + + clearTemporaryFiles(file.path ?? 'image/assets/images'); + return res.json({ message: 'Image saved successfully', url: secure_url }); } catch (error: unknown) { - return res.json({ message: (error).message }); + rollbar.error(error as LogArgument); + return res + .status(500) + .json({ error: 'An error occurred while uploading the image' }); } } /** * @description Get a random waifu from the collection! - * @param { Response } res object with the waifu + * @param { Response } res - object with the waifu * @query size - number of items to retrieve * @query tag_id - tag to filter * @returns { Promise } An url with the waifu image hosted in cloudinary */ async getRandomImage(req: Request, res: Response): Promise { try { - const { size, tag_id } = req.query as unknown as Query; - const getImages = await ImageService.getImage(size, tag_id); - return res.json(getImages); - } catch { - return res.json({ message: 'No se pudo encontrar alguna imagen' }); + const { size, tag_id } = req.query as Query; + const images = await ImageService.getImage(size, tag_id); + return res.json(images); + } catch (error: unknown) { + rollbar.error(error as LogArgument); + return res + .status(500) + .json({ error: 'An error occurred while getting the image' }); } } @@ -58,11 +72,12 @@ class ImageController { */ async getImages(req: Request, res: Response): Promise { try { - const { tag_id } = req.query as unknown as Query; + const { tag_id } = req.query as Query; const images = await ImageService.getAllImages(tag_id); return res.json(images); } catch (error: unknown) { - return res.json({ message: (error).message }); + rollbar.error(error as LogArgument); + return res.json({ error }); } } } diff --git a/src/image/image-repository.ts b/src/image/image-repository.ts index 7b8fdd6..458f53a 100644 --- a/src/image/image-repository.ts +++ b/src/image/image-repository.ts @@ -2,15 +2,15 @@ import Image from './schema/image-schema'; import Tag from '../tag/schema/tag-schema'; import { hasTag } from '../common/utils/ref'; -import { IImage } from './interfaces/image-interface'; +import { ImageProp } from './interfaces/image-interface'; class ImageRepository { /** * @description Creates a new Image in the database - * @param {IImage} image - the image to create in the database - * @return { Promise } - A new image created + * @param { ImageProp } image - the image to create in the database + * @return { Promise } - A new image created */ - async create(image: IImage): Promise { + async create(image: ImageProp): Promise { const tagExists = await Tag.findOne({ tag_id: { $eq: image.tag } }); const _idTag = tagExists?._id; return Image.create({ @@ -22,9 +22,9 @@ class ImageRepository { /** * @description Get all images from the database * @param tag_id - the id from the tag to retrieve - * @return { Promise } An array of images + * @return { Promise } An array of images */ - async findImages(tag_id?: number): Promise { + async findImages(tag_id?: number): Promise { const images = await Image.find().populate(hasTag(tag_id)); return images.filter(image => image.tag !== null); } diff --git a/src/image/image-service.ts b/src/image/image-service.ts index f3a300b..c140cfe 100644 --- a/src/image/image-service.ts +++ b/src/image/image-service.ts @@ -80,7 +80,7 @@ class ImageService { tag: image.tag, }; }); - const randomArray = urls.sort(randomUrls); + const randomArray = urls.toSorted(randomUrls); const sizeArray = randomArray.slice(0, size); const randomUrl = urls[Math.floor(Math.random() * urls.length)]; return size === undefined || null ? randomUrl : sizeArray; diff --git a/src/image/interfaces/image-interface.ts b/src/image/interfaces/image-interface.ts index 6c74dfc..da4a86b 100644 --- a/src/image/interfaces/image-interface.ts +++ b/src/image/interfaces/image-interface.ts @@ -20,4 +20,3 @@ export interface ImageTypeResponse extends ImageType { } export type ImageProp = Omit; -export type IImage = ImageProp; diff --git a/src/user/interfaces/user-interface.ts b/src/user/interfaces/user-interface.ts index 974a186..7e2033b 100644 --- a/src/user/interfaces/user-interface.ts +++ b/src/user/interfaces/user-interface.ts @@ -1,3 +1,6 @@ +import { Boom } from "@hapi/boom"; +import { NextFunction, Response } from "express"; + export interface UsernameType { username: string; } @@ -22,3 +25,5 @@ export type UserPicture = Omit< IUser, "username" | "password" | "save" | "isAdmin" >; + +export type MiddlewareUser = Boom | NextFunction | Response | void; \ No newline at end of file diff --git a/src/user/user-middleware.ts b/src/user/user-middleware.ts index ac482f1..78dce49 100644 --- a/src/user/user-middleware.ts +++ b/src/user/user-middleware.ts @@ -1,76 +1,97 @@ // External Modules -import boom, { Boom } from '@hapi/boom'; +import boom from '@hapi/boom'; import { NextFunction, Request, Response } from 'express'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; +import { LogArgument } from 'rollbar'; // Internal Modules import User from './schema/user-schema'; -import { Config } from '../app/config/config'; -import { UsernameType } from './interfaces/user-interface'; +import { Config } from 'src/app/config/config'; +import { rollbar } from 'src/app/config/rollbar'; +import { MiddlewareUser } from './interfaces/user-interface'; /** - * @description Validates if the user exist in the database + * Checks if a user with the given username already exists in the database + * @param {Object} req.body - request body containing the username * @param {string} req.body.username - the username to check - * @returns {Response} Response with the next function + * @param {Response} res - response object + * @param {NextFunction} next - next function + * @returns {Promise} */ const userExists = async ( req: Request, res: Response, next: NextFunction, -): Promise => { - const { username }: UsernameType = req.body; - const user: UsernameType | null = await User.findOne({ - username: { $eq: username }, - }); - return user ? res.json(boom.conflict('User already exists')) : next(); +): Promise => { + const { username } = req.body; + + try { + const user = await User.findOne({ username: { $eq: username } }); + + if (user) { + return res.status(409).json({ error: 'User already exists' }); + } + + return next(); + } catch (error: unknown) { + rollbar.error(error as LogArgument); + return res.status(500).json({ error }); + } }; /** * @description Validates the user username & password of the request body * @param {string} req.body.username - the username to validate * @param {string} req.body.password - the password to validate - * @returns {Response} if the username & password match, then next() else return an error */ const validateUser = async ( req: Request, res: Response, next: NextFunction, -): Promise => { +): Promise => { try { const { username, password } = req.body; - const userExists = await User.findOne({ username: { $eq: username } }); - const user = userExists?.username; - const pass = userExists?.password; - const isMatch = pass && (await bcrypt.compare(password, pass)); // Compare the password with the hash password - const isValid = user && isMatch; - return isValid ? next() : res.json(boom.badRequest('Invalid credentials')); + const user = await User.findOne({ username: { $eq: username } }); + + if (user?.password && (await bcrypt.compare(password, user.password))) { + return next(); + } + + return res.status(401).json(boom.unauthorized('Invalid credentials')); } catch (error) { - return res.status(400) && res.json(boom.badRequest('Something went wrong')); + return res.status(400).json(boom.badRequest('User not found')); } }; /** * @description Validate if the user is administrator * @param {string} req.body.username - the username to validate - * @returns {Promise} if the user is admin then next() else return an error + * @returns {Promise} if the user is admin then next() else return an error */ const isAdmin = async ( req: Request, res: Response, next: NextFunction, -): Promise => { +): Promise => { + const { username } = req.body; + try { - const { username } = req.body; const user = await User.findOne({ username: { $eq: username } }); - return user?.isAdmin ? next() : res.json(boom.unauthorized('Not admin')); + + if (user?.isAdmin) { + return next(); + } + + return res.status(401).json(boom.unauthorized('Not admin')); } catch (error) { - return res.status(400) && res.json(boom.badRequest('User not found')); + return res.status(400).json(boom.badRequest('User not found')); } }; /** - * @description Validate the jwt + * @description Validate the JWT * @param {Authorization} req.headers - Authorization header with the token * @returns {Response} Authorization error or next */ @@ -78,7 +99,7 @@ const validateToken = async ( req: Request, res: Response, next: NextFunction, -): Promise => { +): Promise => { const { secret } = Config.jwt; try { const { authorization } = req.headers; @@ -88,7 +109,7 @@ const validateToken = async ( return decoded ? next() : res.json(boom.unauthorized()); } } catch (error) { - return res.status(401).json(boom.unauthorized('Invalid token')); + return res.status(401).json(boom.unauthorized()); } };