From a4ac33f71d8a54943bbd91caa116f621fb91d706 Mon Sep 17 00:00:00 2001 From: erenfn Date: Fri, 9 Aug 2024 21:30:01 +0300 Subject: [PATCH] banner backend --- backend/index.js | 2 + backend/src/controllers/banner.controller.js | 166 +++++++++++++++++++ backend/src/models/Banner.js | 52 ++++++ backend/src/models/PopupLog.js | 38 ++--- backend/src/models/index.js | 4 + backend/src/routes/banner.routes.js | 12 ++ backend/src/service/banner.service.js | 39 +++++ 7 files changed, 294 insertions(+), 19 deletions(-) create mode 100644 backend/src/controllers/banner.controller.js create mode 100644 backend/src/models/Banner.js create mode 100644 backend/src/routes/banner.routes.js create mode 100644 backend/src/service/banner.service.js diff --git a/backend/index.js b/backend/index.js index 349c5075..cbfd1c45 100644 --- a/backend/index.js +++ b/backend/index.js @@ -13,6 +13,7 @@ const userRoutes = require('./src/routes/user.routes'); const mocks = require('./src/routes/mocks.routes'); const popup = require('./src/routes/popup.routes'); const popup_log = require('./src/routes/popup_log.routes'); +const banner = require('./src/routes/banner.routes'); // const tourRoutes = require('./src/routes/tour.routes'); const app = express(); @@ -40,6 +41,7 @@ app.use('/users', userRoutes); app.use('/mock/', mocks); app.use('/popup', popup); app.use('/popup_log', popup_log); +app.use('/banner', banner); // app.use('/tours', tourRoutes); app.use((err, req, res, next) => { diff --git a/backend/src/controllers/banner.controller.js b/backend/src/controllers/banner.controller.js new file mode 100644 index 00000000..79cb2643 --- /dev/null +++ b/backend/src/controllers/banner.controller.js @@ -0,0 +1,166 @@ +const bannerService = require("../service/banner.service.js"); +const { internalServerError } = require("../utils/errors"); +const db = require("../models"); +const Banner = db.Banner; + +const validatePosition = (value) => { + const validPositions = ["top", "bottom"]; + return validPositions.includes(value); +}; + +const validateCloseButtonAction = (value) => { + const validActions = ["no-action", "open-url", "open-url-new-tab"]; + return validActions.includes(value); +}; + +const validateColorCode = (value) => { + const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + return colorRegex.test(value); +}; + +class BannerController { + async addBanner(req, res) { + const userId = req.user.id; + const { position, closeButtonAction, fontColor, backgroundColor } = req.body; + + if (!position || !closeButtonAction) { + return res + .status(400) + .json({ + errors: [{ msg: "position and closeButtonAction are required" }], + }); + } + + if (!validatePosition(position) || !validateCloseButtonAction(closeButtonAction)) { + return res + .status(400) + .json({ + errors: [{ msg: "Invalid value entered" }], + }); + } + + const colorFields = { fontColor, backgroundColor }; + for (const [field, value] of Object.entries(colorFields)) { + if (value && !validateColorCode(value)) { + return res + .status(400) + .json({ + errors: [{ msg: `${field} must be a valid hex color code` }], + }); + } + } + + try { + const newBannerData = { ...req.body, createdBy: userId }; + const newBanner = await bannerService.createBanner(newBannerData); + res.status(201).json(newBanner); + } catch (err) { + console.log(err); + const { statusCode, payload } = internalServerError( + "CREATE_BANNER_ERROR", + err.message, + ); + res.status(statusCode).json(payload); + } + } + + async deleteBanner(req, res) { + try { + const { id } = req.params; + + if (isNaN(id) || id.trim() === "") { + return res.status(400).json({ errors: [{ msg: "Invalid id" }] }); + } + + const deletionResult = await bannerService.deleteBanner(id); + + if (!deletionResult) { + return res + .status(400) + .json({ + errors: [{ msg: "Banner with the specified id does not exist" }], + }); + } + + res + .status(200) + .json({ message: `Banner with ID ${id} deleted successfully` }); + } catch (err) { + const { statusCode, payload } = internalServerError( + "DELETE_BANNER_ERROR", + err.message, + ); + res.status(statusCode).json(payload); + } + } + + async editBanner(req, res) { + try { + const { id } = req.params; + + if (!req.body.position || !req.body.closeButtonAction) { + return res + .status(400) + .json({ + errors: [{ msg: "position and closeButtonAction are required" }], + }); + } + + const positionColumn = Banner.tableAttributes.position; + + if (!positionColumn.validate.isIn[0].includes(req.body.position)) { + return res + .status(400) + .json({ errors: [{ msg: "Invalid value for position" }] }); + } + + const closeButtonActionColumn = Banner.tableAttributes.closeButtonAction; + + if ( + !closeButtonActionColumn.validate.isIn[0].includes( + req.body.closeButtonAction, + ) + ) { + return res + .status(400) + .json({ errors: [{ msg: "Invalid value for closeButtonAction" }] }); + } + + const colorFields = ["fontColor", "backgroundColor"]; + const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + for (const field of colorFields) { + if (req.body[field] && !colorRegex.test(req.body[field])) { + return res + .status(400) + .json({ + errors: [{ msg: `${field} must be a valid hex color code` }], + }); + } + } + + const updatedBanner = await bannerService.updateBanner(id, req.body); + res.status(200).json(updatedBanner); + } catch (err) { + const { statusCode, payload } = internalServerError( + "EDIT_BANNER_ERROR", + err.message, + ); + res.status(statusCode).json(payload); + } + } + + async getAllBanners(req, res) { + try { + const banners = await bannerService.getAllBanners(); + res.status(200).json(banners); + } catch (err) { + const { statusCode, payload } = internalServerError( + "GET_ALL_BANNERS_ERROR", + err.message, + ); + res.status(statusCode).json(payload); + } + } +} + +module.exports = new BannerController(); diff --git a/backend/src/models/Banner.js b/backend/src/models/Banner.js new file mode 100644 index 00000000..7ee2aeef --- /dev/null +++ b/backend/src/models/Banner.js @@ -0,0 +1,52 @@ +module.exports = (sequelize, DataTypes) => { + const Banner = sequelize.define('Banner', { + closeButtonAction: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isIn: [["no-action", "open-url", "open-url-new-tab"]], + }, + }, + position: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isIn: [["top", "bottom"]], + }, + }, + url: { + type: DataTypes.STRING, + allowNull: true, + }, + fontColor: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "#FFFFFF", + }, + backgroundColor: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "#FFFFFF", + }, + + createdBy: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: "users", + key: "id", + }, + }, + }, + { + tableName: "banners", + timestamps: false, + }, + ); + + Banner.associate = (models) => { + Banner.belongsTo(models.User, { foreignKey: "createdBy", as: "creator" }); + }; + return Banner; +}; + diff --git a/backend/src/models/PopupLog.js b/backend/src/models/PopupLog.js index 7f499a0c..5ac6b3f3 100644 --- a/backend/src/models/PopupLog.js +++ b/backend/src/models/PopupLog.js @@ -1,24 +1,24 @@ module.exports = (sequelize, DataTypes) => { const PopupLog = sequelize.define('PopupLog', { - popupType: { - type: DataTypes.ENUM('guide', 'tooltip', 'hotspot', 'checklist'), - allowNull: false, - }, - userId: { - type: DataTypes.STRING, - allowNull: false, - }, - showingTime: { - type: DataTypes.DATE, - defaultValue: DataTypes.NOW, - }, - completed: { - type: DataTypes.BOOLEAN, - defaultValue: false, - } + popupType: { + type: DataTypes.ENUM('guide', 'tooltip', 'hotspot', 'checklist'), + allowNull: false, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + showingTime: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + completed: { + type: DataTypes.BOOLEAN, + defaultValue: false, + } }, { - timestamps: false, - tableName: 'popup_logs' + timestamps: false, + tableName: 'popup_logs' }); -return PopupLog; + return PopupLog; }; diff --git a/backend/src/models/index.js b/backend/src/models/index.js index ad731494..ecbd9cff 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -26,9 +26,13 @@ db.User = require("./User.js")(sequelize, Sequelize.DataTypes); db.Popup = require("./Popup.js")(sequelize, Sequelize.DataTypes); db.Token = require("./Token.js")(sequelize, Sequelize.DataTypes); db.PopupLog = require("./PopupLog.js")(sequelize, Sequelize.DataTypes); +db.Banner = require("./Banner.js")(sequelize, Sequelize.DataTypes); // Define associations here db.User.hasMany(db.Popup, { foreignKey: "createdBy", as: "popups" }); db.Popup.belongsTo(db.User, { foreignKey: "createdBy", as: "creator" }); +db.User.hasMany(db.Banner, { foreignKey: "createdBy", as: "banners" }); +db.Banner.belongsTo(db.User, { foreignKey: "createdBy", as: "creator" }); + module.exports = db; diff --git a/backend/src/routes/banner.routes.js b/backend/src/routes/banner.routes.js new file mode 100644 index 00000000..5a0a6b20 --- /dev/null +++ b/backend/src/routes/banner.routes.js @@ -0,0 +1,12 @@ +const express = require("express"); +const bannerController = require("../controllers/banner.controller.js"); +const authenticateJWT = require("../middleware/auth.middleware"); + +const router = express.Router(); + +router.post("/add_banner", authenticateJWT, bannerController.addBanner); +router.delete("/delete_banner/:id", authenticateJWT, bannerController.deleteBanner); +router.put("/edit_banner/:id", authenticateJWT, bannerController.editBanner); +router.get("/all_banners", authenticateJWT, bannerController.getAllBanners); + +module.exports = router; diff --git a/backend/src/service/banner.service.js b/backend/src/service/banner.service.js new file mode 100644 index 00000000..9c26e1a2 --- /dev/null +++ b/backend/src/service/banner.service.js @@ -0,0 +1,39 @@ +const db = require("../models"); +const Banner = db.Banner; + +class BannerService { + async getAllBanners() { + return await Banner.findAll({ + include: [{ model: db.User, as: "creator" }], + }); + } + + async createBanner(data) { + return await Banner.create(data); + } + + async deleteBanner(id) { + const rowsAffected = await Banner.destroy({ where: { id } }); + + if (rowsAffected === 0) { + return false; + } + + return true; + } + + async updateBanner(id, data) { + const [affectedRows, updatedBanners] = await Banner.update(data, { + where: { id }, + returning: true, + }); + + if (affectedRows === 0) { + return null; + } + + return updatedBanners[0]; + } +} + +module.exports = new BannerService();