From f4c6fe18a521c446f0d0d676d872d14828ce7292 Mon Sep 17 00:00:00 2001 From: Wyatt Cox Date: Wed, 4 Mar 2026 17:25:23 -0500 Subject: [PATCH] Add files via upload --- 20260218_1200_create_main_tables.sql | 47 +++++ EventFormsController.js | 143 +++++++++++++ EventFormsUtils.js | 301 +++++++++++++++++++++++++++ 3 files changed, 491 insertions(+) create mode 100644 20260218_1200_create_main_tables.sql create mode 100644 EventFormsController.js create mode 100644 EventFormsUtils.js diff --git a/20260218_1200_create_main_tables.sql b/20260218_1200_create_main_tables.sql new file mode 100644 index 0000000..1a62557 --- /dev/null +++ b/20260218_1200_create_main_tables.sql @@ -0,0 +1,47 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS labels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT UNIQUE NOT NULL +); + +CREATE TABLE IF NOT EXISTS printers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + location TEXT, + description TEXT, + latitude REAL, + longitude REAL +); + +CREATE TABLE IF NOT EXISTS printer_labels ( + printer_id INTEGER NOT NULL, + label_id INTEGER NOT NULL, + PRIMARY KEY (printer_id, label_id), + FOREIGN KEY (printer_id) REFERENCES printers(id) ON DELETE CASCADE, + FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS event_forms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + netid TEXT NOT NULL, + event_type TEXT NOT NULL, + start_date DATETIME, + end_date DATETIME, + organization_name TEXT, + about TEXT, + location TEXT NOT NULL, + image_url TEXT, + approval_status TEXT NOT NULL DEFAULT 'pending' CHECK(approval_status IN ('pending', 'approved', 'rejected')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TRIGGER trg_event_forms_updated_at +AFTER UPDATE ON event_forms +FOR EACH ROW +BEGIN + UPDATE event_forms + SET updated_at = CURRENT_TIMESTAMP + WHERE id = OLD.id; +END; \ No newline at end of file diff --git a/EventFormsController.js b/EventFormsController.js new file mode 100644 index 0000000..d8a477c --- /dev/null +++ b/EventFormsController.js @@ -0,0 +1,143 @@ +import express from "express"; +import multer from "multer"; +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; +import { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms, toPublicEvent, eventsByNetid, getImageByFormId } from "../utils/EventFormsUtils.js"; + +const router = express.Router(); +const upload = multer({ storage: multer.memoryStorage() }); + +// 1. Initialize the S3 Client for DigitalOcean +const s3Client = new S3Client({ + endpoint: "https://nyc3.digitaloceanspaces.com", + forcePathStyle: false, + region: "us-east-1", + credentials: { + accessKeyId: process.env.FORM_S3_ACCESS_KEY, //MUST INCLUDE THIS FOR IT TO WORK (located in .env) + secretAccessKey: process.env.FORM_S3_SECRET_KEY + } +}); + +// Create an event form +router.post("/events/create-event", upload.single('image'), async (req, res) => { + + try { + const { netid, name, eventType, startDate, endDate, organizationName, location, about } = req.body; + + //access the image from req.file + const file = req.file; + let fileUrl = null; + + //check if image was actually uploaded + if (file) { + const params = { + Bucket: process.env.FORM_IMAGE_BUCKET_NAME, + Key: `events/${Date.now()}_${file.originalname}`, + Body: file.buffer, + ACL: "public-read", + ContentType: file.mimetype + }; + // Upload to DigitalOcean + await s3Client.send(new PutObjectCommand(params)); + fileUrl = `https://${process.env.FORM_IMAGE_BUCKET_NAME}.nyc3.digitaloceanspaces.com/${params.Key}`; + //EDIT to have the correct path for correct folder + + } + + const eventForm = await createEventForm({ netid, name, eventType, startDate, endDate, organizationName, location, about, image_url: fileUrl}); + + // Broadcast a notification to the admin room that the event form has been created + const io = req.app.get("io"); + io.to("admin").emit("eventForm:new", {message: "Event request submitted", event: eventForm}); + + // Return the event form to the requesting client + res.status(201).json({ success: true, message: "Your event request has been submitted", data: toPublicEvent(eventForm)}); + } catch (error) { + console.error("Error creating event form:", error); + res.status(400).json({ success: false, message: "Error submitting event request", error: error.message }); + } +}); + +// Get all event forms +router.get("/events/", async (req, res) => { + try { + const eventForms = await getAllEventForms(); + res.status(200).json({ success: true, message: "All event requests retrieved successfully", data: eventForms.map(toPublicEvent)}); + } catch (error) { + console.error("Error getting all event forms:", error.message); + res.status(400).json({ success: false, message: "Error getting all event requests", error: error.message }); + } +}); + +// Update an event form +// NOTE: Only admins can update event forms +// NOTE: id is the event form's id, stored as the primary key in the database +router.put("/events/:id", async (req, res) => { + try { + const { id } = req.params; // id is found in the url path + const { approvalStatus } = req.body; + + // Initalize the io instance + const io = req.app.get("io"); + + // Update the event form in the database + const eventForm = await updateEventForm({ id: parseInt(id), approvalStatus }); + + // Handle event approval (currentl assumes that an update is only for approval or rejection, excludes pending) + if (approvalStatus === "approved") { + // Send a notification to everyone (and the admin room) + io.to("public").emit("eventForm:update", {message: "Event approved", event: toPublicEvent(eventForm)}); + } else { + // Send a notification to the submitting user (and the admin room) that the event was rejected + io.to(`netid:${eventForm.netid}`).emit("eventForm:update", {message: "Your event request has been rejected", event: toPublicEvent(eventForm)}); + } + + // Return the event form to the admin client that requested the update + res.status(200).json({ success: true, message: "Event request updated successfully", data: eventForm}); + } catch (error) { + console.error("Error updating event form:", error.message); + res.status(400).json({ success: false, message: "Error updating event request", error: error.message }); + } +}); + +// Get all approved event forms +router.get("/events/approved", async (req, res) => { + try { + const eventForms = await getApprovedEventForms(); + res.status(200).json({ success: true, message: "All approved event requests retrieved successfully", data: eventForms.map(toPublicEvent)}); + } catch (error) { + console.error("Error getting all approved event requests:", error.message); + res.status(400).json({ success: false, message: "Error getting all approved event requests", error: error.message }); + } +}); + +// Get event forms by netID +router.get("/events/:netid", async (req, res) => { + try { + const {netid} = req.params; // netid is found in the url path + const eventForms = await eventsByNetid(netid); + res.status(200).json({ success: true, message: "All events matching the netid retrieved successfully", data: eventForms.map(toPublicEvent)}); + } catch (error) { + console.error("Error getting all events matching the netid:", error.message); + res.status(400).json({ success: false, message: "Error getting all approved event requests", error: error.message }); + } +}); + +// Get image by event form ID +router.get("/events/images/:id", async (req, res) => { + try { + const {id} = req.params; // id is found in the url path + const url = await getImageByFormId(id); + if (url == null){ + res.status(200).json({ success: true, message: "This form does not contain an image", data: null}); + } + else + { + res.status(200).json({ success: true, message: "The image requested by id retrieved successfully", data: url}); + } + } catch (error) { + console.error("Error getting all approved event requests:", error.message); + res.status(400).json({ success: false, message: "Error getting all approved event requests", error: error.message }); + } +}); + +export default router; \ No newline at end of file diff --git a/EventFormsUtils.js b/EventFormsUtils.js new file mode 100644 index 0000000..0e5e60e --- /dev/null +++ b/EventFormsUtils.js @@ -0,0 +1,301 @@ +import sqlite3 from "sqlite3"; +import { fileURLToPath } from "url"; +import path from "path"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const dbPath = path.join(__dirname, "..", "data", "transit.db"); + +const ALLOWED_EVENT_TYPES = ['temporary', 'permanent']; +const ALLOWED_APPROVAL_STATUSES = ['pending', 'approved', 'rejected']; + +/** + * Creates an event form in the database. + * + * @param eventForm - The event form to create. + * @returns {Promise} - The created event form. + * @throws {Error} - If the event form is invalid. + * @throws {Error} - If the database connection fails. + */ + +function createEventForm({ netid, name, eventType, startDate = null, endDate = null, organizationName = null, location, about = null, image_url }) { + // Safety checks - make sure the event form is valid + if (!netid || !name || !eventType || !location) { + throw new Error("Invalid event form — netid, name, event type, and location are required"); + }; + + // Ensures event type is valid + // if (!ALLOWED_EVENT_TYPES.includes(eventType)) { + // throw new Error('Invalid event form — event type invalid'); + // } + + // Handle event types + if (eventType == 'temporary') { + // If the event is temporary (e.g., tabling), then we require event's date(s) and times, and name of the hosting organization + if (!startDate || !endDate) { + // NOTE: The start and end dates are required for temporary events + throw new Error("Invalid event form — start and end dates are required for temporary events"); + } + if (!organizationName) { + // NOTE: The organization name is required for temporary events + throw new Error("Invalid event form — organization name is required for temporary events"); + } + } + + // Create the event form + const eventForm = { + netid, + name, + eventType, + startDate, + endDate, + organizationName, + location, + image_url, + about, + approvalStatus: 'pending' + }; + + return new Promise((resolve, reject) => { + // Open the database + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `INSERT INTO event_forms (name, netid, event_type, start_date, end_date, organization_name, location, approval_status, about, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + const values = [eventForm.name, eventForm.netid, eventForm.eventType, eventForm.startDate, eventForm.endDate, eventForm.organizationName, eventForm.location, eventForm.approvalStatus, eventForm.about, eventForm.image_url]; + + // Insert the event form into the database + db.run(query, values, function (err) { + if (err) { + db.close(); + console.error(err.message); + return reject(err); + } + + // Get the inserted event form + db.get(`SELECT * FROM event_forms WHERE id = ?`, [this.lastID], (err, row) => { + db.close(); + if (err) { + console.error(err.message); + return reject(err); + } + return resolve(row); + }); + }); + }); +} + +/** + * Gets all event forms from the database. + * + * @returns {Promise>} - The event forms. + * @throws {Error} - If the database connection fails. + */ +function getAllEventForms() { + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `SELECT * FROM event_forms`; + db.all(query, (err, rows) => { + db.close(); + if (err) { + console.error(err.message); + return reject(err); + } + return resolve(rows); + }); + }); +} + +/** + * Updates the approval status of a specified event form in the database. + * + * Allowed approval statuses are: 'pending', 'approved', 'rejected'. + * + * @param integer id - The id of the event form to update. + * @param {Object} approvalStatus - The approval status to update the event form to. + * @returns {Promise} - The updated event form. + * @throws {Error} - If the event form is invalid or the approval status is invalid. + * @throws {Error} - If the event form is not found. + */ +function updateEventForm({ id, approvalStatus }) { + // Safety checks - make sure the event form is valid + if (!id || !approvalStatus) throw new Error("Invalid event form — id and approval status are required"); + // Ensures approval status is valid + if (!ALLOWED_APPROVAL_STATUSES.includes(approvalStatus)) { + throw new Error('Invalid event form — approval status invalid'); + } + + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `UPDATE event_forms SET approval_status = ? WHERE id = ?`; + const values = [approvalStatus, id]; + + // Update the event form in the database + db.run(query, values, function (err) { + if (err) { + db.close(); + console.error(err.message); + return reject(err); + } + + // Checks if there were no updates to the event form (in which case, there was an error) + if (this.changes === 0) { + db.close(); + return reject(new Error("Event form not found")); + } + + // Get the updated event form + db.get(`SELECT * FROM event_forms WHERE id = ?`, [id], (err, row) => { + db.close(); + if (err) { + console.error(err.message); + return reject(err); + } + return resolve(row); + }); + }); + }); +} + +/** + * Gets all approved event forms from the database. + * + * @returns {Promise>} - The approved event forms. + * @throws {Error} - If the database connection fails. + */ +function getApprovedEventForms() { + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `SELECT * FROM event_forms WHERE approval_status = 'approved'`; + db.all(query, (err, rows) => { + if (err) { + console.error(err.message); + return reject(err); + } + resolve(rows); + }); + + // Close the database + db.close((err) => { + if (err) console.error(err.message); + }); + }); +} + + /** + * Gets all event forms matching a specific netID from the database. + * + * @param {Object} netID - netID, kept as netID to prevent + * @returns {Object} - The events matching the netid or a message stateing that there are no matching events to that netid. + */ +function eventsByNetid(netID) { + //check if a netid was passed in + if (!netID) throw new Error("Invalid netid — non-null netid is required"); + + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `SELECT * FROM event_forms WHERE netid = ?`; + db.all(query, [netID],(err, rows) => { + if (err) { + console.error(err.message); + return reject(err); + } + db.close((err) => { //close the database + if (err) console.error(err.message); + }); + + resolve(rows); + }); + }); + + } + + /** + * Gets an image url by the form id + * + * @param {Object} id - id + * @returns {Object} - The image url matching to the specific form, or no url found if there form contains no url + */ +function getImageByFormId(ID) { + //check if a ID was passed in + if (!ID) throw new Error("Invalid id — non-null id is required"); + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(err.message); + return reject(err); + } + }); + + // Prepare the query + const query = `SELECT image_url FROM event_forms WHERE id = ?`; + db.get(query, [ID],(err, rows) => { + if (err) { + console.error(err.message); + return reject(err); + } + db.close((err) => { //close the database + if (err) console.error(err.message); + }); + + resolve(rows); + }); + }); + + } + + +/** + * Converts an event form to a public event. + * + * @param {Object} eventForm - The event form to convert. + * @returns {Object} - The public event. + */ +function toPublicEvent({ name, netid, eventType, startDate, endDate, organizationName, about, location, image_url }) { + return { + name, + netid, + eventType, + startDate, + endDate, + organizationName, + about, + location, + image_url + } +} + +export { createEventForm, getAllEventForms, updateEventForm, getApprovedEventForms, toPublicEvent, eventsByNetid, getImageByFormId}; \ No newline at end of file