Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions __tests__/user/__tests__/users.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -46,7 +48,7 @@ describe('INTEGRATION - User Module', () => {
{
statusCode: httpUnauthorized,
error: "Unauthorized",
message: "Invalid token"
message: "Unauthorized"
}
);
})
Expand Down
6 changes: 3 additions & 3 deletions docker/ARM64.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 35 additions & 20 deletions src/image/image-controller.ts
Original file line number Diff line number Diff line change
@@ -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<Response> } 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<Response> } A JSON response with a success message
*/
async uploadFile(req: Request, res: Response): Promise<Response> {
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>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<Response> } An url with the waifu image hosted in cloudinary
*/
async getRandomImage(req: Request, res: Response): Promise<Response> {
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' });
}
}

Expand All @@ -58,11 +72,12 @@ class ImageController {
*/
async getImages(req: Request, res: Response): Promise<Response> {
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>error).message });
rollbar.error(error as LogArgument);
return res.json({ error });
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/image/image-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IImage> } - A new image created
* @param { ImageProp } image - the image to create in the database
* @return { Promise<ImageProp> } - A new image created
*/
async create(image: IImage): Promise<IImage> {
async create(image: ImageProp): Promise<ImageProp> {
const tagExists = await Tag.findOne({ tag_id: { $eq: image.tag } });
const _idTag = tagExists?._id;
return Image.create({
Expand All @@ -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<IImage[]> } An array of images
* @return { Promise<ImageProp[]> } An array of images
*/
async findImages(tag_id?: number): Promise<IImage[]> {
async findImages(tag_id?: number): Promise<ImageProp[]> {
const images = await Image.find().populate(hasTag(tag_id));
return images.filter(image => image.tag !== null);
}
Expand Down
2 changes: 1 addition & 1 deletion src/image/image-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion src/image/interfaces/image-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,3 @@ export interface ImageTypeResponse extends ImageType {
}

export type ImageProp = Omit<ImageProps, "save">;
export type IImage = ImageProp;
5 changes: 5 additions & 0 deletions src/user/interfaces/user-interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Boom } from "@hapi/boom";
import { NextFunction, Response } from "express";

export interface UsernameType {
username: string;
}
Expand All @@ -22,3 +25,5 @@ export type UserPicture = Omit<
IUser,
"username" | "password" | "save" | "isAdmin"
>;

export type MiddlewareUser = Boom | NextFunction | Response | void;
77 changes: 49 additions & 28 deletions src/user/user-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,105 @@
// 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<Boom | NextFunction>} Response with the next function
* @param {Response} res - response object
* @param {NextFunction} next - next function
* @returns {Promise<MiddlewareUser | NextFunction>}
*/
const userExists = async (
req: Request,
res: Response,
next: NextFunction,
): Promise<Boom | NextFunction | Response | unknown> => {
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<MiddlewareUser> => {
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<Boom | NextFunction} if the username && password match then next() else return an error
* @returns {Promise<MiddlewareUser | NextFunction>} if the username & password match, then next() else return an error
*/
const validateUser = async (
req: Request,
res: Response,
next: NextFunction,
): Promise<Boom | NextFunction | Response | unknown> => {
): Promise<MiddlewareUser> => {
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<Boom | NextFunction | Response | unknown>} if the user is admin then next() else return an error
* @returns {Promise<MiddlewareUser | NextFunction>} if the user is admin then next() else return an error
*/
const isAdmin = async (
req: Request,
res: Response,
next: NextFunction,
): Promise<Boom | NextFunction | Response | unknown> => {
): Promise<MiddlewareUser> => {
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<Boom | NextFunction>} Authorization error or next
*/
const validateToken = async (
req: Request,
res: Response,
next: NextFunction,
): Promise<Boom | NextFunction | Response | unknown> => {
): Promise<MiddlewareUser> => {
const { secret } = Config.jwt;
try {
const { authorization } = req.headers;
Expand All @@ -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());
}
};

Expand Down