diff --git a/src/controllers/EventSponsorController.js b/src/controllers/EventSponsorController.js index babee7b..1109f50 100755 --- a/src/controllers/EventSponsorController.js +++ b/src/controllers/EventSponsorController.js @@ -1,5 +1,6 @@ const EventSponsorRepo = require("../repository/sponsor/EventSponsorRepo"); const SponsorRepo = require("../repository/sponsor/SponsorRepo"); +const ImageRepo = require("../repository/image/ImageRepo"); const setDefaultImageDimensions = (tierName) => { switch (tierName.toLowerCase()) { @@ -31,7 +32,7 @@ class EventSponsorController {             id: s.id,             name: s.sponsorName,             website: s.sponsorWebsite, -            image: s.sponsorImageId || "", +            imageUrl: s.Image?.url || "", amount: s.amount ?? 0,             tier: eventSponsor?.SponsorTier?.tier || "",           }; @@ -65,7 +66,7 @@ class EventSponsorController {             id: s.id,             name: s.sponsorName,             website: s.sponsorWebsite, -            image: s.sponsorImageId || "", +            imageUrl: s.Image?.url || "", sponsorTierId: eventSponsor?.sponsorTierId           };         }); @@ -80,16 +81,22 @@ class EventSponsorController { //    Add sponsor to an event     static async addSponsorToEvent(req, res){         try { -          const { sponsorName, sponsorWebsite, image, amount, sponsorTierId, eventId } = req.body; +          const { sponsorName, sponsorWebsite, imageUrl, amount, sponsorTierId, eventId } = req.body; if (!eventId || !sponsorName) { return res.status(400).json({ error: "Missing required fields: eventId and sponsorName are required." }); } + const imageId = await ImageRepo.createImage({url: imageUrl}) + + if (!imageId) { + return res.status(400).json({ error: "Image could not be stored." }); + } +           const result = await EventSponsorRepo.addSponsorToEvent(eventId, {             sponsorName,             sponsorWebsite, -            image, +            sponsorImageId: imageId, amount: Number(amount),             sponsorTierId           }); @@ -105,13 +112,19 @@ class EventSponsorController {     static async updateEventSponsor(req, res){         try{             const sponsorId = req.params.id; -            const { sponsorName, sponsorWebsite, image, amount, sponsorTierId, eventId, ...otherUpdates } = req.body; - +            const { sponsorName, sponsorWebsite, imageUrl, amount, sponsorTierId, eventId, ...otherUpdates } = req.body; + + const imageId = await ImageRepo.createImage({url: imageUrl}) + + if (!imageId) { + return res.status(400).json({ error: "Image could not be stored." }); + } + // --- 1. Separate Updates for Sponsor (Core) Table --- const sponsorUpdates = { ...(sponsorName !== undefined && { sponsorName }), ...(sponsorWebsite !== undefined && { sponsorWebsite }), - ...(image !== undefined && { sponsorImageId: image }), + ...({ sponsorImageId: imageId }), ...(amount !== undefined && { amount: Number(amount) }), }; diff --git a/src/controllers/SponsorImagesController.js b/src/controllers/SponsorImagesController.js new file mode 100644 index 0000000..54885ae --- /dev/null +++ b/src/controllers/SponsorImagesController.js @@ -0,0 +1,103 @@ +const SponsorImage = require('../models/SponsorImage'); +const SponsorRepo = require('../repository/sponsor/SponsorRepo'); + +const SponsorImagesController = { + /** + * Get all images for a specific sponsor. + * @param {object} req - Express request object (expects req.params.sponsorId) + * @param {object} res - Express response object + * @returns {Promise} + */ + async getImagesBySponsorId(req, res) { + try { + const sponsorId = req.params.sponsorId; + // Assuming SponsorRepo has a method to fetch images by sponsor ID + const images = await SponsorRepo.getImagesBySponsorId(sponsorId); + + if (!images || images.length === 0) { + // Return 200 with an empty array if no images are found, or 404 if you prefer strict REST + return res.status(200).json([]); + // return res.status(404).json({ message: "No images found for this sponsor" }); + } + + res.json(images); + } catch (err) { + console.error("Error fetching sponsor images: ", err); + res.status(500).json({ + message: "Failed to fetch sponsor images", + error: err.message + }); + } + }, + + /** + * Create a new image and link it to a sponsor. + * @param {object} req - Express request object (expects req.body: { imageUrl, sponsorId }) + * @param {object} res - Express response object + * @returns {Promise} + */ + async createImage(req, res) { + try { + const { + imageUrl, + sponsorId + } = req.body; + + if (!imageUrl || !sponsorId) { + return res.status(400).json({ + message: "imageUrl and sponsorId are required" + }); + } + + // Assuming SponsorRepo has a method to create a new sponsor image record + const newImage = await SponsorRepo.createSponsorImage({ + imageUrl, + sponsorId + }); + + res.status(201).json({ + message: "Image added successfully", + image: newImage + }); + } catch (err) { + console.error("Error adding sponsor image: ", err); + res.status(500).json({ + message: "Failed to add sponsor image", + error: err.message + }); + } + }, + + /** + * Delete an image by its ID. + * @param {object} req - Express request object (expects req.params.id) + * @param {object} res - Express response object + * @returns {Promise} + */ + async deleteImage(req, res) { + try { + const imageId = req.params.id; + // Assuming SponsorRepo has a method to delete a sponsor image record + const deleted = await SponsorRepo.deleteSponsorImage(imageId); + + if (deleted === 0) { + return res.status(404).json({ + message: "Image not found" + }); + } + + res.json({ + message: "Image deleted successfully", + result: deleted + }); + } catch (err) { + console.error("Error deleting sponsor image: ", err); + res.status(500).json({ + message: "Failed to delete sponsor image", + error: err.message + }); + } + } +}; + +module.exports = SponsorImagesController; \ No newline at end of file diff --git a/src/middleware/validationMiddleware.js b/src/middleware/validationMiddleware.js index c6c9998..29e7775 100644 --- a/src/middleware/validationMiddleware.js +++ b/src/middleware/validationMiddleware.js @@ -1,8 +1,12 @@ const checkBodyForSpecialCharacters = (req, res, next) => { + // Fields we want to skip + const ignoreFields = ["imageUrl"]; + // Blocks characters often used in attacks ($, %, #, <, >, etc.) const specialCharRegex = /[^a-zA-Z0-9\s-',\.:/\?&_=()@]/g for (const key in req.body) { + if (ignoreFields.includes(key)) continue; // Skip validation for fields we don't want to check const value = req.body[key]; // Only check string values diff --git a/src/models/SponsorImage.js b/src/models/SponsorImage.js new file mode 100644 index 0000000..43a629e --- /dev/null +++ b/src/models/SponsorImage.js @@ -0,0 +1,34 @@ +const Image = require('./Image'); + +class SponsorImage extends Image { + + constructor(url, sponsorId, id = null) { + super(url); + this.id = id; + this.sponsorId = sponsorId; + } + + + validate() { + const errors = super.validate(); + + if (this.sponsorId === undefined || this.sponsorId === null) { + errors.push("Missing Sponsor ID"); + } + if (typeof this.sponsorId !== 'number' || this.sponsorId <= 0) { + errors.push("Invalid Sponsor ID"); + } + + return errors; + } + + toJSON() { + return { + id: this.id, + imageUrl: this.url, + sponsorId: this.sponsorId + }; + } +} + +module.exports = SponsorImage; \ No newline at end of file diff --git a/src/repository/config/Models.js b/src/repository/config/Models.js index a78d547..932e634 100644 --- a/src/repository/config/Models.js +++ b/src/repository/config/Models.js @@ -91,6 +91,10 @@ Hardware.hasMany(HardwareImage, { as: 'images' }); HardwareImage.belongsTo(Hardware, { foreignKey: 'hardwareId' }); + +/* SPONSOR/IMAGE Association */ +Sponsor.belongsTo(Image, { foreignKey: "sponsorImageId" }); + // Function to attach model hooks function attachAuditHooks() { const { AuditLog } = sequelize.models; // Grab all the models diff --git a/src/repository/image/ImageRepo.js b/src/repository/image/ImageRepo.js new file mode 100644 index 0000000..22145f8 --- /dev/null +++ b/src/repository/image/ImageRepo.js @@ -0,0 +1,10 @@ +const Image = require("./Image") + +const ImageRepo = { + async createImage(image) { + const result = await Image.create(image); + return result.id; + } +} + +module.exports = ImageRepo \ No newline at end of file diff --git a/src/repository/sponsor/EventSponsorRepo.js b/src/repository/sponsor/EventSponsorRepo.js index 97e55a1..3228431 100644 --- a/src/repository/sponsor/EventSponsorRepo.js +++ b/src/repository/sponsor/EventSponsorRepo.js @@ -2,6 +2,7 @@ const EventSponsor = require("./EventSponsor"); const SponsorRepo = require("./SponsorRepo"); const Sponsor = require("./Sponsor"); const SponsorTier = require("./SponsorTier"); +const Image = require("../image/Image"); const { Op } = require('sequelize'); class EventSponsorRepo { @@ -20,7 +21,12 @@ class EventSponsorRepo { required: false } ] - } + }, + { + model: Image, + attributes: ["id", "url"], + required: false + } ], attributes: ["id", "sponsorName", "sponsorWebsite", "sponsorImageId", "amount"] }); @@ -31,7 +37,7 @@ class EventSponsorRepo { const sponsor = await SponsorRepo.createSponsor({ sponsorName: sponsorData.sponsorName, sponsorWebsite: sponsorData.sponsorWebsite, - sponsorImageId: sponsorData.image || null, + sponsorImageId: sponsorData.sponsorImageId || null, amount: sponsorData.amount }); diff --git a/src/repository/sponsor/SponsorRepo.js b/src/repository/sponsor/SponsorRepo.js index 357e9cd..d5533a7 100644 --- a/src/repository/sponsor/SponsorRepo.js +++ b/src/repository/sponsor/SponsorRepo.js @@ -2,67 +2,93 @@ const { Sponsor, SponsorTier, EventSponsor, Image } = require("../config/Models" const SponsorRepo = { //Sponsor - async findSponsorById(id){ + async findSponsorById(id) { return Sponsor.findOne({ - where: { id }, + where: {id}, attributes: ['id', 'sponsorName', 'sponsorWebsite'], - include: [{ model: SponsorTier, as: "tiers", attributes: ["tier"] }] + include: [{model: SponsorTier, as: "tiers", attributes: ["tier"]}] }); }, async findAllSponsors() { - return Sponsor.findAll({ - attributes: ['id', 'sponsorName', 'sponsorWebsite'], - include: [ - { - model: EventSponsor, - as: 'EventSponsors', + return Sponsor.findAll({ + attributes: ['id', 'sponsorName', 'sponsorWebsite'], include: [ - { model: SponsorTier, as: 'SponsorTier', attributes: ['tier'] } - ] - } + { + model: EventSponsor, + as: 'EventSponsors', + include: [ + {model: SponsorTier, as: 'SponsorTier', attributes: ['tier']} + ] + } // { model: Image, as: 'image', attributes: ['url'] } // adjust field - ] - }); + ] + }); }, - async createSponsor(sponsor){ + async createSponsor(sponsor) { return Sponsor.create(sponsor); }, - async updateSponsor(id, updates){ + async updateSponsor(id, updates) { return Sponsor.update(updates, {where: {id}, individualHooks: true}); }, - async deleteSponsorById(id){ + async deleteSponsorById(id) { return Sponsor.destroy({where: {id}, individualHooks: true}); }, //EventSponsor - async findEventSponsorsById(id){ + async findEventSponsorsById(id) { return EventSponsor.findAll({ - where: { id }, + where: {id}, include: [{model: Sponsor, include: {model: Image}}, {model: SponsorTier}] }); }, - async findEventSponsorsByEvent(eventId){ + async findEventSponsorsByEvent(eventId) { return EventSponsor.findAll({ where: {event_id: eventId}, include: [{model: Sponsor, include: {model: Image}}, {model: SponsorTier}] }); }, - async createEventSponsor(eventSponsor){ + async createEventSponsor(eventSponsor) { return EventSponsor.create(eventSponsor); }, //SponsorTier - async createSponsorTier(sponsorTier){ + async createSponsorTier(sponsorTier) { return SponsorTier.create(sponsorTier); }, - async deleteSponsorTierById(id){ + async deleteSponsorTierById(id) { return SponsorTier.destroy({ - where: { id }, + where: {id}, individualHooks: true }); }, - async getAllSponsorTier(){ + async getAllSponsorTier() { return SponsorTier.findAll({}); + }, + + async getImagesBySponsorId(sponsorId) { + return Image.findAll({ + where: {sponsorId: sponsorId}, + attributes: ['id', 'url', 'sponsorId'] + }); + }, + + /** + * Creates a new sponsor image record in the database. + * @param {object} imageDetails - { imageUrl: string, sponsorId: number } + * @returns {Promise} The newly created SponsorImage instance with its ID. + */ + async createSponsorImage({imageUrl, sponsorId}) { + return Image.create({ + url: imageUrl, + sponsorId: sponsorId + }); + }, + + async deleteSponsorImage(imageId) { + return Image.destroy({ + where: {id: imageId}, + individualHooks: true + }); } } diff --git a/src/routes/SponsorRoutes.js b/src/routes/SponsorRoutes.js index b90b1ac..e9d79e8 100755 --- a/src/routes/SponsorRoutes.js +++ b/src/routes/SponsorRoutes.js @@ -3,10 +3,12 @@ const router = express.Router(); const { checkBodyForSpecialCharacters } = require('../middleware/validationMiddleware'); const EventSponsorController = require('../controllers/EventSponsorController'); +const {createImage, deleteImage, getImagesBySponsorId} = require("../controllers/SponsorImagesController"); +const upload = require("../controllers/UploadController"); router.get("/", EventSponsorController.getEventSponsors); router.post("/", - checkBodyForSpecialCharacters, + checkBodyForSpecialCharacters, EventSponsorController.addSponsorToEvent); router.put("/:id", checkBodyForSpecialCharacters, @@ -18,6 +20,34 @@ router.get("/tiers", EventSponsorController.getSponsorTiers); router.put("/tiers/:id", EventSponsorController.updateSponsorTier); router.post("/tiers", EventSponsorController.addSponsorTier); router.delete("/tiers/:id", EventSponsorController.removeSponsorTier); -router.get("/by-event/:eventId", EventSponsorController.getSponsorsByEvent); +router.get("/by-event/:eventId", EventSponsorController.getSponsorsByEvent); +router.post("/images", + checkBodyForSpecialCharacters, + createImage); +router.delete("/images/:id", + checkBodyForSpecialCharacters, + deleteImage); +router.get("/:sponsorId/images", getImagesBySponsorId); +router.post("/", + checkBodyForSpecialCharacters, + EventSponsorController.addSponsorToEvent); +router.get("/by-event/:eventId", EventSponsorController.getSponsorsByEvent); +router.get("/", EventSponsorController.getEventSponsors); +router.put("/:id", + checkBodyForSpecialCharacters, + EventSponsorController.updateEventSponsor); +router.delete("/:id", + checkBodyForSpecialCharacters, + EventSponsorController.removeEventSponsor); +router.get("/tiers", EventSponsorController.getSponsorTiers); +router.put("/tiers/:id", + checkBodyForSpecialCharacters, + EventSponsorController.updateSponsorTier); +router.post("/tiers", + checkBodyForSpecialCharacters, + EventSponsorController.addSponsorTier); +router.delete("/tiers/:id", + checkBodyForSpecialCharacters, + EventSponsorController.removeSponsorTier); module.exports = router; \ No newline at end of file diff --git a/src/tests/sponsor.test.js b/src/tests/sponsor.test.js index 4be06eb..c7054e0 100644 --- a/src/tests/sponsor.test.js +++ b/src/tests/sponsor.test.js @@ -230,7 +230,7 @@ describe('Event Sponsor Routes', () => { id: mockSponsorWithEventInfo.id, name: mockSponsorWithEventInfo.sponsorName, website: mockSponsorWithEventInfo.sponsorWebsite, - image: mockSponsorWithEventInfo.sponsorImageId || "", + imageUrl: "", sponsorTierId: mockSponsorWithEventInfo.EventSponsors[0].sponsorTierId, };