From 164301dc0e914ca1c95b33644d4af719d24ee214 Mon Sep 17 00:00:00 2001 From: jrohrbaugh3 Date: Wed, 29 Oct 2025 22:15:06 -0400 Subject: [PATCH 1/8] Add delete routine --- src/controllers/EventController.js | 46 ++++++++++++++++++++++++++++-- src/repository/event/EventRepo.js | 4 +++ src/routes/EventRoutes.js | 5 +++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/controllers/EventController.js b/src/controllers/EventController.js index 6178f87..dd20d8c 100644 --- a/src/controllers/EventController.js +++ b/src/controllers/EventController.js @@ -305,7 +305,6 @@ const createActivity = async (req, res) => { } } - const getActivitiesForEvent = async (req, res) => { try { const { id } = req.params; @@ -393,6 +392,47 @@ const editActivity = async (req, res) => { } } +const deleteActivity = async (req, res) => { + try { + const { id } = req.params; + + // Check that the activity id was provided + if (!id) { + return res.status(400).json({ + message: 'Activity ID is required' + }); + } + + // Check if the activity exists + const existingActivity = await EventRepo.findActivityById(id); + if (!existingActivity) { + return res.status(404).json({ + message: 'Activity not found' + }); + } + + // Delete activity + const rowsDeleted = await EventRepo.deleteActivity(id); + + // Check to make sure the activity was deleted + if (rowsDeleted <= 0) { + return res.status(404).json({ + message: 'Activity could not be deleted (not found or already removed)' + }); + } + + return res.status(200).json({ + message: 'Activity deleted successfully', + deletedId: id + }); + } catch (e) { + return res.status(500).json({ + message: 'Error deleting activity', + error: e.message || e + }); + } +} + const createCategory = async (req, res) => { try { @@ -492,5 +532,7 @@ module.exports = { createCategory, getCategoriesForEvent, editCategory, - editActivity + editActivity, + updateEvent, + deleteActivity } \ No newline at end of file diff --git a/src/repository/event/EventRepo.js b/src/repository/event/EventRepo.js index 5a18c42..f9667af 100644 --- a/src/repository/event/EventRepo.js +++ b/src/repository/event/EventRepo.js @@ -36,6 +36,10 @@ const EventRepo = { return Activity.update({ ...newActivity }, { where: { id: newActivity.id }}); }, + async deleteActivity(activityId) { + return Activity.destroy({ where: { id: activityId } }) + }, + async findActivityById(activityId) { return Activity.findOne({ where: { id: activityId } }); }, diff --git a/src/routes/EventRoutes.js b/src/routes/EventRoutes.js index 3cee2b3..acafdb3 100644 --- a/src/routes/EventRoutes.js +++ b/src/routes/EventRoutes.js @@ -13,7 +13,9 @@ const { createCategory, getCategoriesForEvent, editCategory, - editActivity + editActivity, + updateEvent, + deleteActivity } = require('../controllers/EventController') router.post('/create', createEvent) @@ -24,6 +26,7 @@ router.put('/update', editEvent) router.delete('/delete/:id', deleteEvent) router.post('/activity/', createActivity) router.get('/activity/:id', getActivitiesForEvent) +router.delete('/activity/:id', deleteActivity) router.post('/category/', createCategory) router.get('/category/:id', getCategoriesForEvent) router.put('/category', editCategory) From b4c2cdb713e670ce3dc30428d77766e241a0d068 Mon Sep 17 00:00:00 2001 From: jrohrbaugh3 Date: Sun, 9 Nov 2025 13:08:50 -0500 Subject: [PATCH 2/8] Remove unnecessary date validation --- src/models/Event.js | 4 +--- src/tests/event.test.js | 16 ---------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/models/Event.js b/src/models/Event.js index d01366b..e96c3db 100644 --- a/src/models/Event.js +++ b/src/models/Event.js @@ -31,13 +31,11 @@ class Event { errors.eventName = "Name cannot be more than 100 characters"; } - // 3. Validate start date presence, start date is in valid format, and start date is not in the past + // 3. Validate start date presence and start date is in valid format if (!this.startDate) { errors.startDate = "Date is required"; } else if (!dateRegex.test(this.startDate)) { errors.startDate = "Invalid date format"; - } else if (Date.parse(this.startDate) <= new Date()) { - errors.startDate = "Date cannot be in the past" } // 4. Validate end date presence, end date is in valid format, and end date is not before start date diff --git a/src/tests/event.test.js b/src/tests/event.test.js index 366a4ca..9bff674 100644 --- a/src/tests/event.test.js +++ b/src/tests/event.test.js @@ -115,22 +115,6 @@ describe('POST /create', () => { expect(res.body.errors.startDate).toEqual('Invalid date format'); }); - it('returns 400 (start date in past)', async () => { - // Act: send the HTTP request - const res = await request(app) - .post('/event/create') - .send({ - ...validEventCreateRequest, - startDate: '2000-01-01T01:00:00Z' - }); - - // Assert: response checks - expect(res.statusCode).toEqual(400); - expect(res.body).toHaveProperty('message', 'Validation errors occurred'); - expect(Object.keys(res.body.errors).length).toEqual(1); // There should be exactly one validation error - expect(res.body.errors.startDate).toEqual('Date cannot be in the past'); - }); - it('returns 400 (no end date)', async () => { // Act: send the HTTP request const res = await request(app) From 2a7d6691c6708040687a42cc1aea48e4430d199e Mon Sep 17 00:00:00 2001 From: Emily Culp Date: Mon, 1 Dec 2025 08:16:06 -0500 Subject: [PATCH 3/8] Resolved merge conflicts during rebase --- src/app.js | 2 + src/controllers/AuditLogController.js | 43 +++++++++++++++ src/models/AuditLog.js | 30 +++++++++++ src/repository/audit/AuditLog.js | 46 ++++++++++++++++ src/repository/audit/AuditLogRepo.js | 27 ++++++++++ src/repository/config/Models.js | 60 ++++++++++++++++++++- src/repository/event/EventRepo.js | 12 +++-- src/repository/hardware/HardwareRepo.js | 2 + src/repository/sponsor/SponsorRepo.js | 7 +-- src/repository/team/EventParticipantRepo.js | 9 ++-- src/repository/team/TeamRepo.js | 3 +- src/routes/AuditLogRoutes.js | 7 +++ 12 files changed, 234 insertions(+), 14 deletions(-) create mode 100644 src/controllers/AuditLogController.js create mode 100644 src/models/AuditLog.js create mode 100644 src/repository/audit/AuditLog.js create mode 100644 src/repository/audit/AuditLogRepo.js create mode 100644 src/routes/AuditLogRoutes.js diff --git a/src/app.js b/src/app.js index a19a493..25ed612 100644 --- a/src/app.js +++ b/src/app.js @@ -8,6 +8,7 @@ const eventRoutes = require('./routes/EventRoutes'); const hardwareRoutes = require('./routes/HardwareRoutes'); const sponsorRoutes = require('./routes/SponsorRoutes'); const teamRoutes = require('./routes/TeamRoutes'); +const auditLogRoutes = require('./routes/AuditLogRoutes'); const app = express(); const { authMiddleware } = require('./util/JWTUtil'); @@ -27,6 +28,7 @@ app.use('/user', userRoutes) app.use('/event', eventRoutes) app.use('/hardware', hardwareRoutes) app.use('/teams', teamRoutes); +app.use('/audit-logs', auditLogRoutes); // Sponsor Routes app.use('/sponsors', sponsorRoutes); app.use('/api/eventsponsors', sponsorRoutes); diff --git a/src/controllers/AuditLogController.js b/src/controllers/AuditLogController.js new file mode 100644 index 0000000..c67298d --- /dev/null +++ b/src/controllers/AuditLogController.js @@ -0,0 +1,43 @@ +const AuditLogRepo = require("../repository/audit/AuditLogRepo"); + +const getAllLogs = async (req, res) => { + try { + // Get filters sent by frontend + const { + tableName, + userId, + action, + start, + end, + sort = 'DESC', + limit = 100 + } = req.body; + + // Put filters into single object + const filters = { + tableName, + userId, + action, + start, + end, + sort, + limit: parseInt(limit, 10) + }; + + // Retrieve logs from DB based on filters + const logs = await AuditLogRepo.getAllLogs(filters); + + return res.status(200).json({ + message: 'Audit logs retrieved successfully', + count: logs.length, + logs: logs + }); + } catch (e) { + return res.status(500).json({ + message: 'Internal Server Error', + error: e.message || e + }); + } +}; + +module.exports = { getAllLogs }; diff --git a/src/models/AuditLog.js b/src/models/AuditLog.js new file mode 100644 index 0000000..866cf87 --- /dev/null +++ b/src/models/AuditLog.js @@ -0,0 +1,30 @@ +class AuditLog { + constructor( + id, + tableName, + recordId, + action, + oldValue, + newValue, + userId + ) { + this.id = id; + this.tableName = tableName; + this.recordId = recordId; + this.action = action; + this.oldValue = oldValue; + this.newValue = newValue; + this.userId = userId; + } + + validate() { + if (this.action === 'CREATE' && this.oldValue != null) { + throw new Error('CREATE should not have oldValue'); + } + if (this.action === 'DELETE' && this.newValue != null) { + throw new Error('DELETE should not have newValue'); + } + } +} + +module.exports = AuditLog \ No newline at end of file diff --git a/src/repository/audit/AuditLog.js b/src/repository/audit/AuditLog.js new file mode 100644 index 0000000..636a1df --- /dev/null +++ b/src/repository/audit/AuditLog.js @@ -0,0 +1,46 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/index'); + +const AuditLog = sequelize.define( + 'AuditLog', + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + tableName: { + type: DataTypes.STRING, + allowNull: false + }, + recordId: { + type: DataTypes.INTEGER, + allowNull: false + }, + action: { + type: DataTypes.ENUM("CREATE", "UPDATE", "DELETE"), + allowNull: false + }, + oldValue: { + type: DataTypes.JSON, + allowNull: true + }, + newValue: { + type: DataTypes.JSON, + allowNull: true + }, + userId: { // The id of the user who made the change + type: DataTypes.INTEGER, + allowNull: true + } + }, + + { + tableName: 'AuditLog', + timestamps: true, + updatedAt: false, // we don't update audit rows + } +); + +module.exports = AuditLog; \ No newline at end of file diff --git a/src/repository/audit/AuditLogRepo.js b/src/repository/audit/AuditLogRepo.js new file mode 100644 index 0000000..ab2b78e --- /dev/null +++ b/src/repository/audit/AuditLogRepo.js @@ -0,0 +1,27 @@ +const { Op } = require('sequelize'); +const AuditLog = require('./AuditLog'); + +const AuditLogRepo = { + async getAllLogs(filters = {}) { + const where = {}; + + if (filters.tableName) where.tableName = filters.tableName; + if (filters.userId) where.userId = filters.userId; + if (filters.action) where.action = filters.action; + + // Date range filtering + if (filters.start || filters.end) { + where.createdAt = {}; + if (filters.start) where.createdAt[Op.gte] = new Date(filters.start); + if (filters.end) where.createdAt[Op.lte] = new Date(filters.end); + } + + return AuditLog.findAll({ + where, + order: [['createdAt', filters.sort === 'ASC' ? 'ASC' : 'DESC']], + limit: filters.limit || 100 + }); + }, +}; + +module.exports = AuditLogRepo; diff --git a/src/repository/config/Models.js b/src/repository/config/Models.js index 31c95a5..b0ef035 100644 --- a/src/repository/config/Models.js +++ b/src/repository/config/Models.js @@ -13,9 +13,9 @@ const Team = require('../team/Team'); const HackCategory = require('../event/HackCategory'); const Prize = require('../event/Prize'); const Analytics = require('../analytics/Analytics'); - const Image = require('../image/Image'); const Activity = require('../event/Activity'); +const AuditLog = require('../audit/AuditLog'); /* EVENT ASSOCIATIONS */ @@ -85,6 +85,61 @@ EventParticipant.belongsTo(Team, { foreignKey: 'teamId' }); EventParticipant.belongsTo(User, { foreignKey: 'userId', as: 'userDetails' }); User.hasMany(EventParticipant, { foreignKey: 'userId' }); +// Function to attach model hooks +function attachAuditHooks() { + const { AuditLog } = sequelize.models; // Grab all the models + const ignored = ['AuditLog', 'User']; // We don't want to audit the audit table itself or include participant actions + const cleanData = (obj) => { // Remove sensitive/unnecessary information like password + if (!obj) return null; + const data = obj.toJSON(); + delete data.password; + return data; + }; + + for (const modelName of Object.keys(sequelize.models)) { + if (ignored.includes(modelName)) continue; + + const model = sequelize.models[modelName]; + + // after create + model.addHook('afterCreate', async (instance, options) => { + await AuditLog.create({ + tableName: modelName, + recordId: instance.id, + action: 'CREATE', + newValue: cleanData(instance), + userId: options.userId || null + }); + }); + + // before update + model.addHook('beforeUpdate', async (instance, options) => { + const previous = await model.findByPk(instance.id); + await AuditLog.create({ + tableName: modelName, + recordId: instance.id, + action: 'UPDATE', + oldValue: previous ? cleanData(previous) : null, + newValue: cleanData(instance), + userId: options.userId || null + }); + }); + + // before destroy + model.addHook('beforeDestroy', async (instance, options) => { + await AuditLog.create({ + tableName: modelName, + recordId: instance.id, + action: 'DELETE', + oldValue: cleanData(instance), + userId: options.userId || null + }); + }); + } +} + +attachAuditHooks(); + // Export models module.exports = { sequelize, @@ -101,5 +156,6 @@ module.exports = { Image, Analytics, Hardware, - HardwareImage + HardwareImage, + AuditLog }; diff --git a/src/repository/event/EventRepo.js b/src/repository/event/EventRepo.js index f9667af..4c64e40 100644 --- a/src/repository/event/EventRepo.js +++ b/src/repository/event/EventRepo.js @@ -9,7 +9,7 @@ const EventRepo = { }, async updateEvent(event) { - return Event.update({ ...event }, { where: { id: event.id } }) + return Event.update({ ...event }, { where: { id: event.id }, individualHooks: true }) }, async findEventById(eventId) { @@ -25,7 +25,7 @@ const EventRepo = { }, async deleteEvent(eventId) { - return Event.destroy({ where: { id: eventId } }) + return Event.destroy({ where: { id: eventId }, individualHooks: true }) }, async createActivity(activity) { @@ -33,7 +33,7 @@ const EventRepo = { }, async updateActivity(newActivity) { - return Activity.update({ ...newActivity }, { where: { id: newActivity.id }}); + return Activity.update({ ...newActivity }, { where: { id: newActivity.id }, individualHooks: true }); }, async deleteActivity(activityId) { @@ -50,7 +50,8 @@ const EventRepo = { async createCategory( category) { const event = await Event.findOne({ - where: { id: category.eventId } + where: { id: category.eventId }, + individualHooks: true }) if (!event) { return null @@ -68,7 +69,8 @@ const EventRepo = { return HackCategories.update( {...category}, { - where: { id: category.id } + where: { id: category.id }, + individualHooks: true } ) }, diff --git a/src/repository/hardware/HardwareRepo.js b/src/repository/hardware/HardwareRepo.js index 019c4ba..0bca212 100755 --- a/src/repository/hardware/HardwareRepo.js +++ b/src/repository/hardware/HardwareRepo.js @@ -99,6 +99,7 @@ const HardwareRepo = { async updateHardware(id, updatedFields) { return Hardware.update(updatedFields, { where: { id }, + individualHooks: true }); }, @@ -106,6 +107,7 @@ const HardwareRepo = { async deleteHardware(id) { return Hardware.destroy({ where: { id }, + individualHooks: true }); }, diff --git a/src/repository/sponsor/SponsorRepo.js b/src/repository/sponsor/SponsorRepo.js index 4557048..357e9cd 100644 --- a/src/repository/sponsor/SponsorRepo.js +++ b/src/repository/sponsor/SponsorRepo.js @@ -28,10 +28,10 @@ const SponsorRepo = { return Sponsor.create(sponsor); }, async updateSponsor(id, updates){ - return Sponsor.update(updates, {where: {id}}); + return Sponsor.update(updates, {where: {id}, individualHooks: true}); }, async deleteSponsorById(id){ - return Sponsor.destroy({where: {id}}); + return Sponsor.destroy({where: {id}, individualHooks: true}); }, //EventSponsor @@ -57,7 +57,8 @@ const SponsorRepo = { }, async deleteSponsorTierById(id){ return SponsorTier.destroy({ - where: { id } + where: { id }, + individualHooks: true }); }, async getAllSponsorTier(){ diff --git a/src/repository/team/EventParticipantRepo.js b/src/repository/team/EventParticipantRepo.js index 90556c8..eaf7246 100644 --- a/src/repository/team/EventParticipantRepo.js +++ b/src/repository/team/EventParticipantRepo.js @@ -61,7 +61,8 @@ class EventParticipantRepo { where: { userId: userId, eventId: eventId - } + }, + individualHooks: true } ); return rowsUpdated > 0; @@ -89,7 +90,8 @@ class EventParticipantRepo { where: { userId: { [Op.in]: membersToAssign }, eventId: eventId, - } + }, + individualHooks: true } ); } @@ -104,7 +106,8 @@ class EventParticipantRepo { userId: { [Op.in]: membersToUnassign }, eventId: eventId, teamId: teamId, - } + }, + individualHooks: true } ); } diff --git a/src/repository/team/TeamRepo.js b/src/repository/team/TeamRepo.js index c5264f6..ef2e72f 100644 --- a/src/repository/team/TeamRepo.js +++ b/src/repository/team/TeamRepo.js @@ -25,7 +25,8 @@ const TeamRepo = { const [rowsUpdated] = await Team.update( teamData, { - where: {id: teamId} + where: {id: teamId}, + individualHooks: true } ); return rowsUpdated; diff --git a/src/routes/AuditLogRoutes.js b/src/routes/AuditLogRoutes.js new file mode 100644 index 0000000..d1152ff --- /dev/null +++ b/src/routes/AuditLogRoutes.js @@ -0,0 +1,7 @@ +const express = require('express') +const router = express.Router() +const { getAllLogs } = require('../controllers/AuditLogController') + +router.post('/search', getAllLogs); + +module.exports = router; \ No newline at end of file From 049a62c80d96a8782c0bf76e04667632b9ef8ede Mon Sep 17 00:00:00 2001 From: jrohrbaugh3 Date: Tue, 11 Nov 2025 00:31:36 -0500 Subject: [PATCH 4/8] Fix audit hooks being added before Sequelize finishes registering all models --- src/app.js | 2 ++ src/repository/config/Models.js | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app.js b/src/app.js index 25ed612..a3e7935 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,7 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const { sequelize } = require('./repository/config/index'); // <-- destructure the instance +const { attachAuditHooks } = require('./repository/config/Models'); const userRoutes = require('./routes/UserRoutes'); const eventRoutes = require('./routes/EventRoutes'); const hardwareRoutes = require('./routes/HardwareRoutes'); @@ -45,6 +46,7 @@ async function startServer() { if (process.env.NODE_ENV !== 'test') { // Sync only in non-test environments await sequelize.sync({ alter: true }); + attachAuditHooks(); console.log('✅ Database synchronized successfully.'); } app.listen(port, () => { diff --git a/src/repository/config/Models.js b/src/repository/config/Models.js index b0ef035..450293e 100644 --- a/src/repository/config/Models.js +++ b/src/repository/config/Models.js @@ -88,6 +88,11 @@ User.hasMany(EventParticipant, { foreignKey: 'userId' }); // Function to attach model hooks function attachAuditHooks() { const { AuditLog } = sequelize.models; // Grab all the models + if (!AuditLog) { + console.warn("AuditLog model not yet initialized. Hooks not attached."); + return; + } + const ignored = ['AuditLog', 'User']; // We don't want to audit the audit table itself or include participant actions const cleanData = (obj) => { // Remove sensitive/unnecessary information like password if (!obj) return null; @@ -138,11 +143,10 @@ function attachAuditHooks() { } } -attachAuditHooks(); - // Export models module.exports = { sequelize, + attachAuditHooks, User, Team, Event, From db68097cd6882ca472e11c931caf5f1c0bd4a2a8 Mon Sep 17 00:00:00 2001 From: jrohrbaugh3 Date: Mon, 17 Nov 2025 18:24:09 -0500 Subject: [PATCH 5/8] #42: Bug fixes --- src/controllers/AuditLogController.js | 30 +++++++++++++-------------- src/repository/audit/AuditLogRepo.js | 15 +++++++++++--- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/controllers/AuditLogController.js b/src/controllers/AuditLogController.js index c67298d..78778b1 100644 --- a/src/controllers/AuditLogController.js +++ b/src/controllers/AuditLogController.js @@ -3,34 +3,34 @@ const AuditLogRepo = require("../repository/audit/AuditLogRepo"); const getAllLogs = async (req, res) => { try { // Get filters sent by frontend - const { + let { tableName, userId, action, start, end, sort = 'DESC', - limit = 100 + limit, + page } = req.body; - // Put filters into single object - const filters = { - tableName, - userId, - action, - start, - end, - sort, - limit: parseInt(limit, 10) - }; + // Put filters into single object and clean values + const filters = { tableName, userId, action, start, end, sort }; + limit = Number(limit) || 100; + page = Number(page) || 1; // Retrieve logs from DB based on filters - const logs = await AuditLogRepo.getAllLogs(filters); + const { count, logs } = await AuditLogRepo.getAllLogs(filters, limit, page); return res.status(200).json({ message: 'Audit logs retrieved successfully', - count: logs.length, - logs: logs + logs: logs, + pagination: { + page: page, + limit: limit, + totalCount: count, + totalPages: Math.ceil(count / limit) + } }); } catch (e) { return res.status(500).json({ diff --git a/src/repository/audit/AuditLogRepo.js b/src/repository/audit/AuditLogRepo.js index ab2b78e..9a8e322 100644 --- a/src/repository/audit/AuditLogRepo.js +++ b/src/repository/audit/AuditLogRepo.js @@ -2,7 +2,7 @@ const { Op } = require('sequelize'); const AuditLog = require('./AuditLog'); const AuditLogRepo = { - async getAllLogs(filters = {}) { + async getAllLogs(filters = {}, limit = 100, page = 1) { const where = {}; if (filters.tableName) where.tableName = filters.tableName; @@ -16,11 +16,20 @@ const AuditLogRepo = { if (filters.end) where.createdAt[Op.lte] = new Date(filters.end); } - return AuditLog.findAll({ + // Get offset using current page + const offset = (page - 1) * limit; + + const result = await AuditLog.findAndCountAll({ where, order: [['createdAt', filters.sort === 'ASC' ? 'ASC' : 'DESC']], - limit: filters.limit || 100 + limit: limit, + offset: offset }); + + return { + count: result.count, + logs: result.rows + } }, }; From 6583bb82172cc00a2c011fa1e473b612b9a98cb6 Mon Sep 17 00:00:00 2001 From: jrohrbaugh3 Date: Tue, 11 Nov 2025 10:16:25 -0500 Subject: [PATCH 6/8] Revert .env changes --- package.json | 2 +- src/util/emailService.js | 50 ++++++++++++++++------------------------ 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index fdaf885..9abcc04 100755 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "", "license": "ISC", "dependencies": { + "@getbrevo/brevo": "^3.0.1", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "cors": "^2.8.5", @@ -22,7 +23,6 @@ "express": "^4.19.2", "jsonwebtoken": "^9.0.2", "mysql2": "^3.14.5", - "nodemailer": "^7.0.9", "sequelize": "^6.37.3", "sequelize-cli": "^6.6.3" }, diff --git a/src/util/emailService.js b/src/util/emailService.js index 7e14731..ea36ff2 100644 --- a/src/util/emailService.js +++ b/src/util/emailService.js @@ -1,37 +1,27 @@ -const nodemailer = require('nodemailer'); -const path = require('path'); +const brevo = require('@getbrevo/brevo'); -const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - user: process.env.APP_EMAIL, - pass: process.env.APP_EMAIL_PASSWORD, // Gmail app password - }, -}); +const apiInstance = new brevo.TransactionalEmailsApi(); +apiInstance.authentications['apiKey'].apiKey = process.env.EMAIL_API_KEY; async function sendRegistrationConfirmation(to, firstName) { - const mailOptions = { - from: `"YCP Hacks" <${process.env.APP_EMAIL}>`, - to, - subject: 'Welcome to YCP Hacks!', - html: ` -

Hi ${firstName},

-

Thanks for registering for YCP Hacks!

-

We’re excited to have you join us! Visit the YCP Hacks website to stay up-to-date on event details and announcements.

-
-

- The YCP Hacks Team

- YCP Hacks Logo - `, - attachments: [ - { - filename: 'ycphacks_logo.png', - path: path.join(__dirname, '../assets/ycphacks_logo.png'), - cid: 'logo' - } - ] + const sendSmtpEmail = new brevo.SendSmtpEmail(); + + sendSmtpEmail.to = [{ email: to }]; + sendSmtpEmail.sender = { email: process.env.FROM_EMAIL, name: 'YCP Hacks' }; + + // This is the ID of the template we created through Brevo's website + sendSmtpEmail.templateId = 1; + + // These correspond to variables in the template + sendSmtpEmail.params = { + firstName: firstName }; - await transporter.sendMail(mailOptions); + try { + await apiInstance.sendTransacEmail(sendSmtpEmail); + } catch (error) { + console.error('Error sending email:', error); + } } -module.exports = { sendRegistrationConfirmation }; \ No newline at end of file +module.exports = { sendRegistrationConfirmation }; From 0fbe360e5f6c2cf166be71460ffeeddfdbe2b0c7 Mon Sep 17 00:00:00 2001 From: Emily Culp Date: Mon, 1 Dec 2025 08:52:57 -0500 Subject: [PATCH 7/8] #60: Linked TR to Events --- .../EventParticipantsController.js | 72 +++++++++++++++++-- src/controllers/TeamController.js | 4 +- src/repository/config/Models.js | 4 +- src/repository/team/EventParticipantRepo.js | 6 +- src/repository/team/TeamRepo.js | 33 ++++++++- src/routes/EventRoutes.js | 1 + src/routes/TeamRoutes.js | 2 +- src/tests/team.test.js | 10 +-- 8 files changed, 112 insertions(+), 20 deletions(-) diff --git a/src/controllers/EventParticipantsController.js b/src/controllers/EventParticipantsController.js index 773fb51..d3293c6 100644 --- a/src/controllers/EventParticipantsController.js +++ b/src/controllers/EventParticipantsController.js @@ -1,4 +1,6 @@ const EventParticipantsRepo = require('../repository/team/EventParticipantRepo'); +const TeamRepo = require('../repository/team/TeamRepo') +const EventRepo = require('../repository/event/EventRepo') class EventParticipantController{ static async getUnassignedParticipants(req, res) { @@ -8,15 +10,15 @@ class EventParticipantController{ const participants = await EventParticipantsRepo.findUnassignedParticipants(eventId); const nonBannedParticipants = participants.filter(p => - p.userDetails && p.userDetails.isBanned !== true && p.userDetails.isBanned !== 1 + p.participants && p.participants.isBanned !== true && p.participants.isBanned !== 1 ); const formattedParticipants = nonBannedParticipants.map(p => ({ id: p.userId, - firstName: p.userDetails?.firstName, - lastName: p.userDetails?.lastName, - email: p.userDetails?.email, - checkIn: p.userDetails?.checkIn, + firstName: p.participants?.firstName, + lastName: p.participants?.lastName, + email: p.participants?.email, + checkIn: p.participants?.checkIn, teamId: p.teamId })); @@ -94,6 +96,66 @@ class EventParticipantController{ return res.status(500).json({ message: 'Server error retrieving team status' }); } } + static async getTeamsByEvent(req, res) { + try { + // 1. Get eventId from query parameters + let { eventId } = req.query; + + if (eventId === 'undefined' || eventId === '') { + const activeEvent = await EventRepo.findActiveEvent(); + + if (!activeEvent) { + return res.status(404).json({ error: "No eventId provided and no active event found." }); + } + eventId = activeEvent.id; + } + + if (!eventId) { + return res.status(500).json({ error: "Internal error: Failed to determine event ID." }); + } + + // 2. Call a new repository function to get teams filtered by event + const teams = await TeamRepo.getTeamsByEvent(eventId); + + // 3. Map the data for the response + const teamData = teams.map(team => ({ + id: team.id, + teamName: team.teamName, + presentationLink: team.presentationLink || null, + githubLink: team.githubLink || null, + projectName: team.projectName || null, + projectDescription: team.projectDescription || null, + + // Map the team members list + participants: team.EventParticipants ? + team.EventParticipants.map(participant => { + const user = participant.participants; + + // Safety check remains valid + if (!user) { + console.warn(`Participant record in team ${team.id} is missing User details.`); + return 'Unknown User'; + } + + return { + id: user.id, + firstName: user.firstName, + lastName: user.lastName + }; + }) + : [] + })); + + res.status(200).json({ + message: `Successfully fetched teams for event ${eventId}`, + data: teamData + }); + + } catch (err) { + console.error('Error getting teams by event:', err); + res.status(500).json({ message: 'Error getting teams by event', error: err.message }); + } + } } module.exports = EventParticipantController; \ No newline at end of file diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index 69198a2..0ec7257 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -77,8 +77,8 @@ class TeamController { const participants = await EventParticipantsRepo.findParticipantsByTeamId(teamId); const formattedParticipants = participants.map(p => ({ - id: p.userDetails.id, - name: `${p.userDetails.firstName} ${p.userDetails.lastName}` + id: p.participants.id, + name: `${p.participants.firstName} ${p.participants.lastName}` })); return { diff --git a/src/repository/config/Models.js b/src/repository/config/Models.js index 450293e..ff18c74 100644 --- a/src/repository/config/Models.js +++ b/src/repository/config/Models.js @@ -79,10 +79,10 @@ HackCategory.belongsTo(Event, { onDelete: 'CASCADE', }); -Team.hasMany(EventParticipant, { foreignKey: 'teamId' }); +Team.hasMany(EventParticipant, { foreignKey: 'teamId', as: 'EventParticipants' }); EventParticipant.belongsTo(Team, { foreignKey: 'teamId' }); -EventParticipant.belongsTo(User, { foreignKey: 'userId', as: 'userDetails' }); +EventParticipant.belongsTo(User, { foreignKey: 'userId', as: 'participants', targetKey: 'id' }); User.hasMany(EventParticipant, { foreignKey: 'userId' }); // Function to attach model hooks diff --git a/src/repository/team/EventParticipantRepo.js b/src/repository/team/EventParticipantRepo.js index eaf7246..27d01fc 100644 --- a/src/repository/team/EventParticipantRepo.js +++ b/src/repository/team/EventParticipantRepo.js @@ -9,7 +9,7 @@ class EventParticipantRepo { where: { teamId: teamId }, include: [{ model: User, - as: 'userDetails', + as: 'participants', attributes: ['id', 'firstName', 'lastName', 'isBanned'], where: { isBanned: { [Op.not]: true } @@ -24,7 +24,7 @@ class EventParticipantRepo { include: [{ model: User, - as: 'userDetails', + as: 'participants', attributes: [], where: { isBanned: { [Op.not]: true } @@ -44,7 +44,7 @@ class EventParticipantRepo { }, include: [{ model: User, - as: 'userDetails', + as: 'participants', attributes: ['id', 'firstName', 'lastName', 'email', 'checkIn', 'isBanned'] , where: { checkIn: 1, diff --git a/src/repository/team/TeamRepo.js b/src/repository/team/TeamRepo.js index ef2e72f..6ca65c8 100644 --- a/src/repository/team/TeamRepo.js +++ b/src/repository/team/TeamRepo.js @@ -1,4 +1,4 @@ -const { Team } = require('../config/Models'); +const { EventParticipant, User, Team } = require('../config/Models'); const TeamRepo = { // Method to create a new Team @@ -32,7 +32,7 @@ const TeamRepo = { return rowsUpdated; }, async delete(teamId){ - return Team.destroy({where: {id: teamId}}); + return Team.destroy({where: {id: teamId}}); }, async findProjectDetailsById(teamId) { try { @@ -73,6 +73,35 @@ const TeamRepo = { console.error('Repo error updating project details:', error); throw error; } + }, + async getTeamsByEvent(eventId) { + return await Team.findAll({ + where: { + eventId: eventId + }, + include: [ + { + model: EventParticipant, + as: 'EventParticipants', + attributes: ['userId', 'teamId'], + include: [{ + model: User, + as: 'participants', + attributes: ['id', 'firstName', 'lastName'], + required: true + }] + } + ], + // Select the specific fields needed for the response mapping + attributes: [ + 'id', + 'teamName', + 'presentationLink', + 'githubLink', + 'projectName', + 'projectDescription' + ], + }); } } diff --git a/src/routes/EventRoutes.js b/src/routes/EventRoutes.js index acafdb3..8d10bf0 100644 --- a/src/routes/EventRoutes.js +++ b/src/routes/EventRoutes.js @@ -31,5 +31,6 @@ router.post('/category/', createCategory) router.get('/category/:id', getCategoriesForEvent) router.put('/category', editCategory) router.put('/activity', editActivity) +router.put('/update', updateEvent) module.exports = router; \ No newline at end of file diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 5150ab4..8679ef2 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -5,7 +5,7 @@ const TeamController = require('../controllers/TeamController'); const EventParticipantController = require('../controllers/EventParticipantsController'); router.post('/create', TeamController.createTeam); -router.get('/all', TeamController.getAllTeams); +router.get('/all', EventParticipantController.getTeamsByEvent); router.get('/unassignedParticipants', EventParticipantController.getUnassignedParticipants); router.put('/unassign', EventParticipantController.unassignParticipant); router.put('/:id', TeamController.updateTeam); diff --git a/src/tests/team.test.js b/src/tests/team.test.js index f9eef67..e760225 100644 --- a/src/tests/team.test.js +++ b/src/tests/team.test.js @@ -33,12 +33,12 @@ const mockEventParticipantsData = [ { userId: MOCK_USER_ID_1, teamId: MOCK_TEAM_ID, - userDetails: mockParticipant1, + participants: mockParticipant1, }, { userId: MOCK_USER_ID_2, teamId: MOCK_TEAM_ID, - userDetails: mockParticipant2, + participants: mockParticipant2, }, ]; @@ -417,9 +417,9 @@ describe('EventParticipantController', () => { // -------------------------------------------------------------------------- describe('GET /teams/unassignedParticipants', () => { const mockUnassigned = [ - { userId: 301, teamId: null, eventId: 1, userDetails: { id: 301, firstName: 'A', lastName: 'Unassigned', email: 'a@example.com', checkIn: true, isBanned: false } }, - { userId: 302, teamId: null, eventId: 1, userDetails: { id: 302, firstName: 'B', lastName: 'Unassigned', email: 'b@example.com', checkIn: false, isBanned: 0 } }, - { userId: 303, teamId: null, eventId: 1, userDetails: { id: 303, firstName: 'C', lastName: 'Banned', email: 'c@example.com', checkIn: true, isBanned: true } }, // Should be filtered out + { userId: 301, teamId: null, eventId: 1, participants: { id: 301, firstName: 'A', lastName: 'Unassigned', email: 'a@example.com', checkIn: true, isBanned: false } }, + { userId: 302, teamId: null, eventId: 1, participants: { id: 302, firstName: 'B', lastName: 'Unassigned', email: 'b@example.com', checkIn: false, isBanned: 0 } }, + { userId: 303, teamId: null, eventId: 1, participants: { id: 303, firstName: 'C', lastName: 'Banned', email: 'c@example.com', checkIn: true, isBanned: true } }, // Should be filtered out ]; const expectedFormatted = [ { id: 301, firstName: 'A', lastName: 'Unassigned', email: 'a@example.com', checkIn: true, teamId: null }, From 7e150b7b4e58790cd082cb3d89d55504eb7ca1bf Mon Sep 17 00:00:00 2001 From: Emily Culp Date: Tue, 2 Dec 2025 05:04:54 -0500 Subject: [PATCH 8/8] Removed .env file --- .env | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 256b6e4..0000000 --- a/.env +++ /dev/null @@ -1,9 +0,0 @@ -DB_HOST=ycp_hacks -DB_USER=root -DB_PASSWORD=root -DB_NAME=ycp_hacks -DB_PORT=3306 -APP_PORT=3000 -CORS='*' -APP_EMAIL=ycphacks.register@gmail.com -APP_EMAIL_PASSWORD=zter cbky dxdv ysfz