diff --git a/CustomError.js b/CustomError.js new file mode 100644 index 0000000..ce56a33 --- /dev/null +++ b/CustomError.js @@ -0,0 +1,7 @@ +export class CustomError extends Error { + constructor(message, code) { + super(); + this.message = message; + this.code = code; + } +} \ No newline at end of file diff --git a/constants/response.messages.js b/constants/response.messages.js new file mode 100644 index 0000000..bcb2d39 --- /dev/null +++ b/constants/response.messages.js @@ -0,0 +1,44 @@ +export const MESSAGES = { + USER_MESSAGES: { + ERROR_PASSWORD_LENGHT: "User password must be at least 3 characters long", + ERROR_EMAIL_FORMAT: "Only @gmail email is allowed", + ERROR_EMAIL_UNIQUE: "User email is in use", + ERROR_PHONE_FORMAT: "Only +380xxxxxxxxx phoneNumber format is allowed", + ERROR_PHONE_UNIQUE: "User phone is in use", + UNEXPECTED_ERROR_CREATING: + "Unexpected error creating user. Please, contact or try again", + ERROR_USER_CREDENTIAL_LOGIN: "Wrong user credentials", + ERROR_USER_VALIDATION_MIDDLEWARE: + "Missing body parameters. Required: firstName, lastName, email, phoneNumber, password", + ERROR_AUTH_VALIDATION_MIDDLEWARE: + "Missing body parameters. Required: email, password", + ERROR_USER_NOT_FOUND: "User id not found. No action performed.", + ERROR_USER_UPDATE_EMPTY_PARAMS: + "Required: firstName or lastName or email or phoneNumber or password.", + }, + FIGHTER_MESSAGES: { + ERROR_NAME_UNIQUE: "Fighter name is already in use.", + UNEXPECTED_FIGHTER_CREATING: + "Unexpected error creating fighter. Please, contact or try again", + ERROR_FIGHTER_NOT_FOUND: "Fighter id not found. No action performed.", + ERROR_FIGHTER_CREATE_PARAMS: + "Missing body parameters. Required: name, power, defense", + ERROR_FIGHTER_UPDATE_EMPTY_PARAMS: + "Required: name or power or defense or health.", + ERROR_DEFENSE_RANGE_VALUE: "Fighter defense value must be between 1 to 10.", + ERROR_POWER_RANGE_VALUE: "Fighter power value must be between 1 to 100.", + ERROR_HEALTH_RANGE_VALUE: "Fighter health value must be between 80 to 120.", + }, + FIGHT_MESSAGES: { + ERROR_FIGHT_UPDATE_PARAMS: + "Required: id url param and body params fighter1Shot, fighter2Shot, fighter1Health and fighter2Health", + ERROR_FIGHT_CREATE_PARAMS: "Required: fighter1 and fighter2.", + ERROR_FIGHTER_NOT_FOUND: "Fighter id not found.", + ERROR_FIGHT_NOT_FOUND: "Fight id not found.", + }, + GENERIC_ERROR_MESSAGE: "Unexpected error. Please, contact or try again", + GENERIC_EMPTY_REQUEST_ERROR: "Requested data is not found.", + ERROR_SECURITY_TOKEN_OR_ID_MISSING: "Token or User id missing", + ERROR_SECURITY_TOKEN_BAD_CREDENTIAL: "Invalid token", + ERROR_NUMBER_TYPE_DATA_CAST: "Data type error. Can't cast to number:", +}; \ No newline at end of file diff --git a/helpers/middlewares.helper.js b/helpers/middlewares.helper.js new file mode 100644 index 0000000..8ea21dc --- /dev/null +++ b/helpers/middlewares.helper.js @@ -0,0 +1,28 @@ +export function checkEveryParamExistence(...params) { + return params.every( + (param) => param !== null && param !== undefined && param !== "" + ); +} + +export function checkAtLeastOneParamExist(...params) { + return params.some((param) => param !== null && param !== undefined); +} + +export function filterOnlyExistingParams(ojbParams, currentObj) { + let copyObject = {}; + for (let prop in ojbParams) { + if ( + ojbParams[prop] !== null && + ojbParams[prop] !== undefined && + ojbParams[prop] !== "" + ) { + copyObject[prop] = ojbParams[prop]; + } + } + + return { ...currentObj, ...copyObject }; +} + +export function emailToLowerCased(email) { + return email.toLowerCase(); +} \ No newline at end of file diff --git a/helpers/services.helper.js b/helpers/services.helper.js new file mode 100644 index 0000000..2f744b0 --- /dev/null +++ b/helpers/services.helper.js @@ -0,0 +1,13 @@ +import { MESSAGES } from "../constants/response.messages.js"; +import { CustomError } from "../CustomError.js"; + +export function castValuesToNumber(value) { + const castedValue = Number(value); + if (isNaN(castedValue)) { + throw new CustomError( + `${MESSAGES.ERROR_NUMBER_TYPE_DATA_CAST} ${value}`, + 400 + ); + } + return castedValue; +} \ No newline at end of file diff --git a/index.js b/index.js index 780f0c3..d1c65d3 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,8 @@ initRoutes(app); app.use("/", express.static("./client/build")); const port = 3050; -app.listen(port, () => {}); +app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); +}); export { app }; diff --git a/middlewares/fight.validation.middleware.js b/middlewares/fight.validation.middleware.js new file mode 100644 index 0000000..3db0cbd --- /dev/null +++ b/middlewares/fight.validation.middleware.js @@ -0,0 +1,39 @@ +import { MESSAGES } from "../constants/response.messages.js"; +import { CustomError } from "../CustomError.js"; +import { checkEveryParamExistence } from "../helpers/middlewares.helper.js"; + +const createFightValid = (req, res, next) => { + const { fighter1, fighter2 } = req.body; + if (checkEveryParamExistence(fighter1, fighter2)) { + return next(); + } + const paramsEroor = new CustomError( + MESSAGES.FIGHT_MESSAGES.ERROR_FIGHT_CREATE_PARAMS, + 400 + ); + return next(paramsEroor); +}; + +const updateFightValid = (req, res, next) => { + const { fighter1Shot, fighter2Shot, fighter1Health, fighter2Health } = + req.body; + const id = req.params.id; + if ( + checkEveryParamExistence( + fighter1Shot, + fighter2Shot, + fighter1Health, + fighter2Health, + id + ) + ) { + return next(); + } + const requestedDataError = new CustomError( + MESSAGES.FIGHT_MESSAGES.ERROR_FIGHT_UPDATE_PARAMS, + 400 + ); + return next(requestedDataError); +}; + +export { createFightValid, updateFightValid }; \ No newline at end of file diff --git a/middlewares/fighter.validation.middleware.js b/middlewares/fighter.validation.middleware.js index 9945224..52240b7 100644 --- a/middlewares/fighter.validation.middleware.js +++ b/middlewares/fighter.validation.middleware.js @@ -1,13 +1,107 @@ import { FIGHTER } from "../models/fighter.js"; +import { fighterService } from "../services/fighterService.js"; -const createFighterValid = (req, res, next) => { +const createFighterValid = async (req, res, next) => { // TODO: Implement validatior for FIGHTER entity during creation - next(); + const { name } = req.body; + + try { + const fighter = await fighterService.getOneFighter({ name }); + + if (fighter) { + throw new Error(`Fighter with name '${name}' already exists.`); + } + + const requiredFields = ["name", "power", "defense"]; + checkRequiredFields(req.body, requiredFields); + checkValidKeys(req.body, Object.keys(FIGHTER)); + isValid(req.body); + + req.validatedData = { ...req.body }; + next(); + } catch (err) { + res.status(400).send({ error: err.message }); + } }; -const updateFighterValid = (req, res, next) => { +const updateFighterValid = async (req, res, next) => { // TODO: Implement validatior for FIGHTER entity during update - next(); + const { id } = req.params; + + try { + const fighter = await fighterService.getOneFighter({ id }); + + if (!fighter) { + return res.status(404).send({ error: `Fighter with id '${id}' was not found.` }); + } + + if (!Object.keys(req.body).length) { + throw new Error("No fields to update."); + } + + if (req.body.name) { + const existing = await fighterService.getOneFighter({ name: req.body.name }); + if (existing && existing.id !== id) { + throw new Error(`Fighter with name '${req.body.name}' already exists.`); + } + } + + checkValidKeys(req.body, Object.keys(FIGHTER)); + isValid(req.body); + + req.validatedData = { ...req.body }; + next(); + } catch (err) { + res.status(400).send({ error: err.message }); + } +}; + +const checkRequiredFields = (body, requiredFields) => { + requiredFields.forEach(field => { + if (body[field] === undefined || body[field] === null || body[field] === "") { + throw new Error(`Field '${field}' is required and cannot be empty.`); + } + }); +}; + +const checkValidKeys = (body, validKeys) => { + Object.keys(body).forEach(key => { + if (!validKeys.includes(key)) { + throw new Error(`Invalid field '${key}' provided.`); + } + }); +}; + +const isValid = (body) => { + if (body.name) { + validateName(body.name); + } + if (body.power !== undefined) { + validatePower(body.power); + } + if (body.defense !== undefined) { + validateDefense(body.defense); + } +}; + +const validateName = (name) => { + if (!/^[a-zA-Z]+$/.test(name)) { + throw new Error("Invalid fighter name. Only letters allowed."); + } +}; + +const validatePower = (power) => { + const num = Number(power); + if (isNaN(num) || num < 0 || num > 100) { + throw new Error("Power must be a number in the range 0 - 100."); + } +}; + +const validateDefense = (defense) => { + const num = Number(defense); + if (isNaN(num) || num < 1 || num > 10) { + throw new Error("Defense must be a number in the range 1 - 10."); + } }; export { createFighterValid, updateFighterValid }; diff --git a/middlewares/response.middleware.js b/middlewares/response.middleware.js index a74208f..1e2635a 100644 --- a/middlewares/response.middleware.js +++ b/middlewares/response.middleware.js @@ -1,6 +1,16 @@ const responseMiddleware = (req, res, next) => { // TODO: Implement middleware that returns result of the query - next(); + + if (res.err) { + const { message } = res.err; + console.log("message", message); + return res.json({ + error: true, + message, + }); + } + console.log("responseMiddleware res.data", res.data); + return res.json(res.data); }; -export { responseMiddleware }; +export { responseMiddleware }; \ No newline at end of file diff --git a/middlewares/user.validation.middleware.js b/middlewares/user.validation.middleware.js index f35e98d..93f52c8 100644 --- a/middlewares/user.validation.middleware.js +++ b/middlewares/user.validation.middleware.js @@ -1,13 +1,132 @@ +import { MESSAGES } from "../constants/response.messages.js"; import { USER } from "../models/user.js"; +import { userService } from "../services/userService.js"; +import { authService } from "../services/authService.js"; +import { CustomError } from "../CustomError.js"; -const createUserValid = (req, res, next) => { +const createUserValid = async (req, res, next) => { // TODO: Implement validatior for USER entity during creation - next(); + + const { email, phoneNumber } = req.body; + + try { + const userEmail = await userService.search({ email }); + const userPhone = await userService.search({ phoneNumber }); + + if (userEmail || userPhone) { + throw new Error("User with this email or phone number already exists."); + } + + const requiredFields = Object.keys(USER).filter((key) => key !== "id"); + const providedFields = Object.keys(req.body); + + if (providedFields.length !== requiredFields.length) { + throw new Error("Invalid number of fields."); + } + + for (const field of providedFields) { + if (!requiredFields.includes(field)) { + throw new Error(`Unexpected field: ${field}`); + } + if (!req.body[field]) { + throw new Error(`Empty field: ${field}`); + } + } + + isValid(req.body); + + res.data = { ...req.body }; + next(); + } catch (err) { + res.err = err; + next(); + } }; -const updateUserValid = (req, res, next) => { +const updateUserValid = async (req, res, next) => { // TODO: Implement validatior for user entity during update - next(); + + const { id } = req.params; + + try { + const existingUser = await userService.search({ id }); + + if (!existingUser) { + res.status(404); + throw new Error("User does not exist."); + } + + if (!Object.keys(req.body).length) { + throw new Error("No fields to update."); + } + + const updateFields = Object.keys(req.body); + const allowedFields = Object.keys(USER).filter((key) => key !== "id"); + + for (const field of updateFields) { + if (!allowedFields.includes(field)) { + throw new Error(`Unexpected field: ${field}`); + } + if (!req.body[field]) { + throw new Error(`Empty field: ${field}`); + } + } + + if (req.body.email) { + const existingEmailUser = await userService.search({ email: req.body.email }); + if (existingEmailUser && existingEmailUser.id !== id) { + throw new Error("Email already in use."); + } + } + + if (req.body.phoneNumber) { + const existingPhoneUser = await userService.search({ phoneNumber: req.body.phoneNumber }); + if (existingPhoneUser && existingPhoneUser.id !== id) { + throw new Error("Phone number already in use."); + } + } + + isValid(req.body); + + res.data = { ...req.body }; + next(); + } catch (err) { + res.err = err; + next(); + } +}; + +const isValid = (body) => { + if (body.email) validateEmail(body.email); + if (body.phoneNumber) validatePhone(body.phoneNumber); + if (body.firstName) validateName(body.firstName); + if (body.lastName) validateName(body.lastName); + if (body.password) validatePassword(body.password); +}; + +const validateEmail = (email) => { + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailPattern.test(email)) { + throw new Error("Invalid email."); + } +}; + +const validatePassword = (password) => { + if (password.length < 3) { + throw new Error("Password must be at least 3 characters."); + } +}; + +const validatePhone = (phone) => { + if (!phone.match(/^\+380\d{9}$/)) { + throw new Error("Invalid phone number. Format: +380XXXXXXXXX"); + } +}; + +const validateName = (name) => { + if (!name.match(/^[a-zA-Z]+$/)) { + throw new Error("Invalid name. Only letters allowed."); + } }; export { createUserValid, updateUserValid }; diff --git a/routes/authRoutes.js b/routes/authRoutes.js index f390a40..3e4af3c 100644 --- a/routes/authRoutes.js +++ b/routes/authRoutes.js @@ -9,7 +9,17 @@ router.post( (req, res, next) => { try { // TODO: Implement login action (get the user if it exist with entered credentials) + const data = authService.login(req.body); + if (!data) { + throw new Error({ + code: 401, + status: "Unauthorized", + message: "Incorrect email/phone or password", + }); + } + res.data = data; + res.status(200); } catch (err) { res.err = err; } finally { @@ -18,5 +28,4 @@ router.post( }, responseMiddleware ); - export { router }; diff --git a/routes/fightRoutes.js b/routes/fightRoutes.js index de8265f..6a247aa 100644 --- a/routes/fightRoutes.js +++ b/routes/fightRoutes.js @@ -1,13 +1,53 @@ import { Router } from "express"; -import { fightersService } from "../services/fightService.js"; +import { fightsService } from "../services/fightService.js"; import { - createUserValid, - updateUserValid, -} from "../middlewares/user.validation.middleware.js"; -import { responseMiddleware } from "../middlewares/response.middleware.js"; + createFightValid, + updateFightValid, +} from "../middlewares/fight.validation.middleware.js"; const router = Router(); // OPTIONAL TODO: Implement route controller for fights +router.get("/", async (req, res, next) => { + try { + const fights = await fightsService.getAllFights(); + return res.json(fights); + } catch (error) { + return next(error); + } +}); -export { router }; +router.get("/:id", async (req, res, next) => { + try { + const fight = await fightsService.getOneFight({ id: req.params.id }); + if (!fight) { + return res.status(404).json({ message: "Fight not found" }); + } + return res.json(fight); + } catch (error) { + return next(error); + } +}); + +router.post("/", createFightValid, (req, res, next) => { + const { fighter1, fighter2 } = req.body; + try { + const fightCreated = fightsService.createFight(fighter1, fighter2); + return res.status(201).json(fightCreated); + } catch (error) { + return next(error); + } +}); + +router.patch("/:id/log", updateFightValid, (req, res, next) => { + const id = req.params.id; + try { + const fightUpdated = fightsService.updateFight(id, req.body); + return res.json(fightUpdated); + } catch (error) { + return next(error); + } +}); + + +export { router }; \ No newline at end of file diff --git a/routes/fighterRoutes.js b/routes/fighterRoutes.js index fba6e53..778b9dc 100644 --- a/routes/fighterRoutes.js +++ b/routes/fighterRoutes.js @@ -1,6 +1,5 @@ import { Router } from "express"; import { fighterService } from "../services/fighterService.js"; -import { responseMiddleware } from "../middlewares/response.middleware.js"; import { createFighterValid, updateFighterValid, @@ -9,5 +8,70 @@ import { const router = Router(); // TODO: Implement route controllers for fighter +router.get("/", async (req, res, next) => { + try { + const storedFighters = await fighterService.getAllFighters(); + res.json(storedFighters); + } catch (error) { + next(error); + } +}); + +router.get("/:id", async (req, res, next) => { + const id = req.params.id; + try { + const requestedFighter = await fighterService.getOneFighter({ id }); + if (!requestedFighter) { + return res.status(404).json({ message: "Fighter not found" }); + } + res.json(requestedFighter); + } catch (error) { + next(error); + } +}); + +router.post( + "/", + createFighterValid, + async (req, res, next) => { + const { name, power, defense, health } = req.body; + try { + const newFighter = await fighterService.createFighter({ + name, + power, + defense, + health, + }); + res.status(201).json(newFighter); + } catch (error) { + next(error); + } + } +); + +router.delete("/:id", async (req, res, next) => { + const id = req.params.id; + try { + const deletedFighter = await fighterService.deleteFighter(id); + res.json(deletedFighter); + } catch (error) { + next(error); + } +}); + +router.patch( + "/:id", + updateFighterValid, + async (req, res, next) => { + const id = req.params.id; + try { + const updatedFighter = await fighterService.updateFighter(id, req.body); + res.json(updatedFighter); + } catch (error) { + next(error); + } + } +); + export { router }; diff --git a/routes/routes.js b/routes/routes.js index b207f15..0d29da7 100644 --- a/routes/routes.js +++ b/routes/routes.js @@ -2,12 +2,14 @@ import { router as userRoutes } from "./userRoutes.js"; import { router as authRoutes } from "./authRoutes.js"; import { router as fighterRoutes } from "./fighterRoutes.js"; import { router as fightRoutes } from "./fightRoutes.js"; +import { responseMiddleware } from "../middlewares/response.middleware.js"; -const initRoutes = (app) => { +const initRoutes = (app) => { app.use("/api/users", userRoutes); app.use("/api/fighters", fighterRoutes); app.use("/api/fights", fightRoutes); app.use("/api/auth", authRoutes); + app.use(responseMiddleware); }; -export { initRoutes }; +export { initRoutes }; \ No newline at end of file diff --git a/routes/userRoutes.js b/routes/userRoutes.js index f5ee661..4b9f036 100644 --- a/routes/userRoutes.js +++ b/routes/userRoutes.js @@ -10,4 +10,129 @@ const router = Router(); // TODO: Implement route controllers for user +router.get( + "/", + async (req, res, next) => { + try { + const users = await userService.getUsers(); + res.data = users; + } catch (err) { + res.err = err; + res.status(500); + } + next(); + }, + responseMiddleware +); + +router.get( + "/:id", + async (req, res, next) => { + try { + const { id } = req.params; + const user = await userService.search({ id }); + + if (!user) { + res.status(404); + throw new Error("User not found"); + } + + res.data = user; + } catch (err) { + res.err = err; + } + next(); + }, + responseMiddleware +); + +router.post( + "/", + createUserValid, + async (req, res, next) => { + try { + const user = await userService.createUser(req.body); + res.status(201); + res.data = user; + } catch (err) { + res.err = err; + res.status(400); + } + next(); + }, + responseMiddleware +); + +router.patch( + "/:id", + updateUserValid, + async (req, res, next) => { + try { + const { id } = req.params; + const existingUser = await userService.search({ id }); + + if (!existingUser) { + res.status(404); + throw new Error("User not found"); + } + + const updatedUser = await userService.updateUser(id, req.body); + res.data = updatedUser; + } catch (err) { + res.err = err; + res.status(400); + } + next(); + }, + responseMiddleware +); + + +router.put( + "/:id", + updateUserValid, + async (req, res, next) => { + try { + const { id } = req.params; + const existingUser = await userService.search({ id }); + + if (!existingUser) { + res.status(404); + throw new Error("User not found"); + } + + const updatedUser = await userService.updateUser(id, req.body); + res.data = updatedUser; + } catch (err) { + res.err = err; + res.status(400); + } + next(); + }, + responseMiddleware +); + +router.delete( + "/:id", + async (req, res, next) => { + try { + const { id } = req.params; + const existingUser = await userService.search({ id }); + + if (!existingUser) { + res.status(404); + throw new Error("User does not exist"); + } + + const deleted = await userService.deleteUser(id); + res.data = deleted; + } catch (err) { + res.err = err; + res.status(400); + } + next(); + }, + responseMiddleware +); + export { router }; diff --git a/services/fightService.js b/services/fightService.js index f53d6d4..50076c5 100644 --- a/services/fightService.js +++ b/services/fightService.js @@ -1,9 +1,43 @@ import { fightRepository } from "../repositories/fightRepository.js"; -class FightersService { - // OPTIONAL TODO: Implement methods to work with fights +class FightService { + async getAllFights() { + const fights = await fightRepository.getAll(); + return fights || []; + } + + async getOneFight(search) { + const fight = await fightRepository.getOne(search); + return fight || null; + } + + async createFight(fighter1, fighter2, log = []) { + console.log("log before saving:", log); + + const newFight = { + fighter1, + fighter2, + log, + createdAt: new Date(), + }; + + const fight = await fightRepository.create(newFight); + console.log("Fight saved:", fight); + + return fight || null; + } + + async updateFight(id, body) { + const fight = await fightRepository.update(id, body); + return fight || null; + } + + async deleteFight(id) { + const fight = await fightRepository.delete(id); + return fight || null; + } } -const fightersService = new FightersService(); +const fightsService = new FightService(); -export { fightersService }; +export { fightsService }; diff --git a/services/fighterService.js b/services/fighterService.js index fb219cd..ccaa441 100644 --- a/services/fighterService.js +++ b/services/fighterService.js @@ -2,6 +2,31 @@ import { fighterRepository } from "../repositories/fighterRepository.js"; class FighterService { // TODO: Implement methods to work with fighters + + async getAllFighters() { + const fighters = await fighterRepository.getAll(); + return fighters || []; + } + + async getOneFighter(search) { + const fighter = await fighterRepository.getOne(search); + return fighter || null; + } + + async createFighter(body) { + const fighter = await fighterRepository.create(body); + return fighter || null; + } + + async updateFighter(id, body) { + const fighter = await fighterRepository.update(id, body); + return fighter || null; + } + + async deleteFighter(id) { + const fighter = await fighterRepository.delete(id); + return fighter || null; + } } const fighterService = new FighterService(); diff --git a/services/userService.js b/services/userService.js index b80b323..9bac5ca 100644 --- a/services/userService.js +++ b/services/userService.js @@ -3,6 +3,39 @@ import { userRepository } from "../repositories/userRepository.js"; class UserService { // TODO: Implement methods to work with user + getUsers() { + const users = userRepository.getAll(); + if (users.length === 0) { + return []; + } + + return users; + } + + createUser(user) { + const users = userRepository.create(user); + if (!users) { + return null; + } + return users; + } + + updateUser(id, body) { + const user = userRepository.update(id, body); + if (!user) { + return null; + } + return user; + } + + deleteUser(id) { + const user = userRepository.delete(id); + if (!user) { + return null; + } + return user; + } + search(search) { const item = userRepository.getOne(search); if (!item) {