diff --git a/server/server/routing/db_routes.js b/server/server/routing/db_routes.js index 2ed38868..a424b0c6 100644 --- a/server/server/routing/db_routes.js +++ b/server/server/routing/db_routes.js @@ -39,6 +39,35 @@ const defaultFileSizeLimit = 15 * 1024 * 1024; const DB_CONFIG = require("../database/db_config"); const CONFIG = require("../config/config"); const { nanoid } = require("nanoid"); +// Split route modules (kept mounted here so existing API paths remain available) +const sponsorsRoutes = require("./routes/sponsors_routes"); +const actionsRoutes = require("./routes/actions_routes"); +const projectsRoutes = require("./routes/projects_routes"); +const archivesRoutes = require("./routes/archives_routes"); +const errorLogsRoutes = require("./routes/error_logs_routes"); +const usersRoutes = require("./routes/users_routes"); +const timeLoggingRoutes = require("./routes/time_logging_routes"); +const submissionsRoutes = require("./routes/submissions_routes"); +const proposalsRoutes = require("./routes/proposals_routes"); +const filesRoutes = require("./routes/files_routes"); +const semesterRoutes = require("./routes/semester_routes"); +const devOnlyRoutes = require("./routes/dev_only_routes"); +const dashboardRoutes = require("./routes/dashboard_routes"); + +// Mount split routers early so they take precedence over the large monolithic definitions +db_router.use("/", sponsorsRoutes(db)); +db_router.use("/", actionsRoutes(db)); +db_router.use("/", projectsRoutes(db)); +db_router.use("/", archivesRoutes(db)); +db_router.use("/", errorLogsRoutes(db)); +db_router.use("/", usersRoutes(db)); +db_router.use("/", timeLoggingRoutes(db)); +db_router.use("/", submissionsRoutes(db)); +db_router.use("/", proposalsRoutes(db)); +db_router.use("/", filesRoutes(db)); +db_router.use("/", semesterRoutes(db)); +db_router.use("/", devOnlyRoutes(db)); +db_router.use("/", dashboardRoutes(db)); const CONSTANTS = require("../consts"); const { ROLES } = require("../consts"); const { off } = require("process"); @@ -56,3829 +85,16 @@ const ACTION_TARGETS = { // Routes module.exports = (db) => { - /** - * /getAllUsersForLogin ENDPOINT SHOULD ONLY BE HIT IN DEVELOPMENT ONLY - * - * THIS IS USED BY THE DEVELOPMENT LOGIN AND SHOULD NOT BE USED FOR ANYTHING ELSE - */ - if (process.env.NODE_ENV !== "production") { - // gets all users - db_router.get("/DevOnlyGetAllUsersForLogin", (req, res) => { - db.query(`SELECT ${CONSTANTS.SIGN_IN_SELECT_ATTRIBUTES} FROM users`).then( - (users) => res.send(users), - ); - }); - //Redeploy database - db_router.put("/DevOnlyRedeployDatabase", async (req, res) => { - try { - await redeployDatabase(); - res - .status(200) - .json({ success: true, message: "Database redeployed successfully" }); - } catch (error) { - res.status(500).json({ - success: false, - message: "Failed to redeploy database", - error: error.message, - }); - } - }); - } - - db_router.get( - "/selectAllSponsorInfo", - [UserAuth.isCoachOrAdmin], - (req, res) => { - db.selectAll(DB_CONFIG.tableNames.sponsor_info).then(function (value) { - res.send(value); - }); - }, - ); - - db_router.get( - "/selectAllStudentInfo", - [UserAuth.isCoachOrAdmin], - (req, res, next) => { - let getStudentsQuery = ` - SELECT * - FROM users - LEFT JOIN semester_group - ON users.semester_group = semester_group.semester_id - WHERE type = 'student' - ORDER BY semester_group desc - `; - db.query(getStudentsQuery) - .then((values) => { - res.send(values); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/selectAllNonStudentInfo", - [UserAuth.isAdmin], - (req, res, next) => { - let getUsersQuery = ` - SELECT * - FROM users - LEFT JOIN semester_group - ON users.semester_group = semester_group.semester_id - WHERE type != 'student' - `; - db.query(getUsersQuery) - .then((values) => { - res.send(values); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getSemesterStudents", - [UserAuth.isSignedIn], - (req, res, next) => { - let query = ""; - let params = []; - switch (req.user.type) { - //Retrieves all users from a semester group that is similar to the student that is making the query. - case ROLES.STUDENT: - query = ` - SELECT users.* - FROM users - WHERE users.semester_group = ( - SELECT semester_group FROM users WHERE system_id = ? - ) AND users.type = 'student'`; - params = [req.user.system_id]; - break; - - case ROLES.COACH: - query = ` - SELECT users.* FROM users - LEFT JOIN semester_group - ON users.semester_group = semester_group.semester_id - WHERE users.semester_group IN ( - SELECT projects.semester FROM projects - WHERE projects.project_id IN ( - SELECT project_coaches.project_id FROM project_coaches - WHERE project_coaches.coach_id = ? - ) - )`; - params = [req.user.system_id]; - break; - - case ROLES.ADMIN: - query = `SELECT * FROM users - LEFT JOIN semester_group - ON users.semester_group = semester_group.semester_id - WHERE users.type = 'student'`; - break; - default: - break; - } - - db.query(query, params) - .then((users) => { - if (req.user.type === ROLES.STUDENT) { - users = users.map((user) => { - let output = {}; - if (user.project === req.user.project) { - output["last_login"] = user["last_login"]; - output["prev_login"] = user["prev_login"]; - } - output["active"] = user["active"]; - output["email"] = user["email"]; - output["fname"] = user["fname"]; - output["lname"] = user["lname"]; - output["project"] = user["project"]; - output["semester_group"] = user["semester_group"]; - output["system_id"] = user["system_id"]; - output["type"] = user["type"]; - return output; - }); - } - res.send(users); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get("/getProjectMembers", [UserAuth.isSignedIn], (req, res) => { - let query = `SELECT users.*, project_coaches.project_id FROM users - LEFT JOIN project_coaches ON project_coaches.coach_id = users.system_id - WHERE users.project = ? OR project_coaches.project_id = ?`; - - params = [req.query.project_id, req.query.project_id]; - - db.query(query, params).then((users) => res.send(users)); - }); - - // NOTE: This is currently used for getting user for AdminView to mock users, however, I feel that this network request will get quite large - // as we add about 100 users every semester. - db_router.get("/getActiveUsers", [UserAuth.isAdmin], (req, res) => { - let query = `SELECT ${CONSTANTS.SIGN_IN_SELECT_ATTRIBUTES} - FROM users - WHERE active = ''`; - db.query(query).then((users) => res.send(users)); - }); - - db_router.post( - "/createUser", - [ - UserAuth.isAdmin, - UserAuth.canWrite, - body("system_id") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("fname") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("lname") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("email") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("type") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("semester_group").isLength({ max: 50 }), - body("project").isLength({ max: 50 }), - body("active").trim().escape().isLength({ max: 50 }), - body("viewOnly").trim().escape().isLength({ max: 50 }), - ], - async (req, res, next) => { - let result = validationResult(req); - console.log(result); - - if (result.errors.length !== 0) { - const error = new Error("Validation Error"); - error.statusCode = 400; - return next(error); - } - - let body = req.body; - - const sql = `INSERT INTO ${DB_CONFIG.tableNames.users} - (system_id, fname, lname, email, type, semester_group, project, active, view_only, profile_info) - VALUES (?,?,?,?,?,?,?,?,?,?)`; - - const active = - body.active === "false" - ? moment().format(CONSTANTS.datetime_format) - : ""; - - const viewOnly = body.viewOnly === "true" ? "TRUE" : "FALSE"; - - // Default profile_info with required fields - const defaultProfileInfo = JSON.stringify({ - additional_info: "", - dark_mode: false, - gantt_view: true, - }); - - const params = [ - body.system_id, - body.fname, - body.lname, - body.email, - body.type, - body.semester_group === "" ? null : body.semester_group, - body.project === "" ? null : body.project, - active, - viewOnly, - defaultProfileInfo, - ]; - db.query(sql, params) - .then(() => { - return res.status(200).send(); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/batchCreateUser", - [ - UserAuth.isAdmin, - UserAuth.canWrite, - // TODO: Add more validation - ], - async (req, res, next) => { - try { - let users = JSON.parse(req.body.users); - const failedUsers = []; - const successUsers = []; - - for (const user of users) { - // Default profile_info with required fields - const defaultProfileInfo = JSON.stringify({ - additional_info: "", - dark_mode: false, - gantt_view: true, - }); - - const values = [ - user.system_id, - user.fname, - user.lname, - user.email, - user.type, - user.semester_group === "" ? null : user.semester_group, - user.active.toLocaleLowerCase() === "false" - ? moment().format(CONSTANTS.datetime_format) - : "", - defaultProfileInfo, - ]; - - try { - await db.query( - `INSERT INTO ${DB_CONFIG.tableNames.users} - (system_id, fname, lname, email, type, semester_group, active, profile_info) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - values, - ); - successUsers.push(user); - } catch (err) { - let errorMessage = err.message; - - // Provide more user-friendly error messages for common constraint violations - if (err.code === "SQLITE_CONSTRAINT") { - if ( - err.message.includes( - "UNIQUE constraint failed: users.system_id", - ) - ) { - errorMessage = `System ID '${user.system_id}' already exists`; - } else if ( - err.message.includes("UNIQUE constraint failed: users.email") - ) { - errorMessage = `Email '${user.email}' already exists`; - } else { - errorMessage = "Duplicate data - user may already exist"; - } - } - - failedUsers.push({ user, error: errorMessage }); - } - } - - res.status(200).json({ successUsers, failedUsers }); - } catch (err) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - }, - ); - - db_router.post( - "/editUser", - [UserAuth.isAdmin, UserAuth.canWrite], - (req, res, next) => { - let body = req.body; - - let updateQuery = ` - UPDATE users - SET fname = ?, - lname = ?, - email = ?, - type = ?, - semester_group = ?, - project = ?, - active = ?, - view_only = ? - WHERE system_id = ? - `; - - const active = - body.active === "false" - ? moment().format(CONSTANTS.datetime_format) - : ""; - - const viewOnly = body.viewOnly === "true" ? "TRUE" : "FALSE"; - - let params = [ - body.fname, - body.lname, - body.email, - body.type, - body.semester_group || null, - body.project || null, - active, - viewOnly, - body.system_id, - ]; - - db.query(updateQuery, params) - .then(() => { - return res.status(200).send(); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/removeTime", - [UserAuth.isSignedIn, UserAuth.canWrite], - (req, res, next) => { - if (!req.body.id) { - const error = new Error("No Id Provided"); - error.statusCode = 400; - return next(error); - } - - const sql = "UPDATE time_log SET active=0 WHERE time_log_id = ?"; - - db.query(sql, [req.body.id]) - .then(() => { - res.status(200).send(); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get("/avgTime", [UserAuth.isSignedIn], async (req, res, next) => { - const sql = - "SELECT ROUND(AVG(CASE WHEN active != 0 THEN time_amount ELSE NULL END), 2) AS avgTime, system_id FROM time_log WHERE project = ? GROUP BY system_id"; - console.log(req.query.project_id); - - db.query(sql, [req.query.project_id]) - .then((time) => { - console.log(time); - res.send(time); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.post( - "/createTimeLog", - [UserAuth.canWrite], - async (req, res, next) => { - let result = validationResult(req); - - if (result.errors.length !== 0) { - const error = new Error("Validation Error"); - error.statusCode = 400; - return next(error); - } - - // Validate that the work date is not in the future - // This prevents users from logging time for dates that haven't occurred yet - const workDate = new Date(req.body.date); - const currentDate = new Date(); - const currentDateOnly = new Date( - currentDate.getFullYear(), - currentDate.getMonth(), - currentDate.getDate(), - ); - const workDateOnly = new Date( - workDate.getFullYear(), - workDate.getMonth(), - workDate.getDate(), - ); - - if (workDateOnly > currentDateOnly) { - const error = new Error("Cannot log time for future dates"); - error.statusCode = 400; - return next(error); - } - - // Validate that the work date is within the past 14 days (2 weeks) - // This maintains the existing business rule about recent time logging - const twoWeeksAgo = new Date(currentDateOnly); - twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); - - if (workDateOnly < twoWeeksAgo) { - const error = new Error("Cannot log time for dates older than 14 days"); - error.statusCode = 400; - return next(error); - } - - let mock_id = req.user.mock ? req.user.mock.system_id : ""; - - const sql = `INSERT INTO time_log - (semester, system_id, project, mock_id, work_date, time_amount, work_comment) - VALUES (?,?,?,?,?,?,?)`; - - const params = [ - req.user.semester_group, - req.user.system_id, - req.user.project, - mock_id, - req.body.date, - req.body.time_amount, - req.body.comment, - ]; - db.query(sql, params) - .then(() => { - return res.status(200).send(); - }) - .catch((err) => { - console.error(err); - let error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getActiveProjects", - [UserAuth.isSignedIn], - (req, res, next) => { - let getProjectsQuery = ` - SELECT * - FROM projects - LEFT JOIN semester_group - ON projects.semester = semester_group.semester_id - WHERE projects.semester IS NOT NULL - `; - db.query(getProjectsQuery) - .then((values) => { - res.send(values); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getActiveCoaches", - [UserAuth.isCoachOrAdmin], - (req, res, next) => { - const sql = `SELECT * FROM users WHERE type = '${ROLES.COACH}' AND active = ''`; - - db.query(sql) - .then((coaches) => { - res.send(coaches); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getProjectCoaches", - [UserAuth.isCoachOrAdmin], - (req, res, next) => { - const getProjectCoaches = `SELECT users.* FROM users - LEFT JOIN project_coaches ON project_coaches.coach_id = users.system_id - WHERE project_coaches.project_id = ?`; - - db.query(getProjectCoaches, [req.query.project_id]) - .then((coaches) => { - res.send(coaches); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getProjectStudents", - [UserAuth.isCoachOrAdmin], - (req, res, next) => { - const getProjectStudents = "SELECT * FROM users WHERE users.project = ?"; - - db.query(getProjectStudents, [req.query.project_id]) - .then((students) => { - res.send(students); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getProjectStudentNames", - [UserAuth.isSignedIn], - (req, res, next) => { - const getProjectStudents = - "SELECT fname,lname FROM users WHERE users.project = ? and users.system_id!=?"; - - db.query(getProjectStudents, [req.query.project_id, req.user.system_id]) - .then((students) => { - res.send(students); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/selectAllCoachInfo", - [UserAuth.isCoachOrAdmin], - (req, res, next) => { - const getCoachInfoQuery = ` - SELECT users.system_id, - users.fname, - users.lname, - users.email, - users.semester_group, - ( - SELECT "[" || group_concat( - "{" || - """title""" || ":" || """" || COALESCE(projects.display_name, projects.title) || """" || "," || - """semester_id""" || ":" || """" || projects.semester || """" || "," || - """project_id""" || ":" || """" || projects.project_id || """" || "," || - """organization""" || ":" || """" || projects.organization || """" || "," || - """status""" || ":" || """" || projects.status || """" || - "}" - ) || "]" - FROM project_coaches - LEFT JOIN projects ON projects.project_id = project_coaches.project_id - WHERE project_coaches.coach_id = users.system_id - ) projects - FROM users - WHERE users.type = "${ACTION_TARGETS.COACH}" - `; - - db.query(getCoachInfoQuery) - .then((coaches) => { - res.send(coaches); - }) - .catch((err) => { - console.error(error); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - // used in the /projects page and home page if featured - db_router.get("/getActiveArchiveProjects", (req, res, next) => { - const { resultLimit, page, featured } = req.query; - let skipNum = page * resultLimit; - let projectsQuery; - let rowCountQuery; - if (featured === "true") { - // home page - randomized order of projects - projectsQuery = `SELECT * FROM ${DB_CONFIG.tableNames.archive} WHERE oid NOT IN - ( SELECT oid FROM ${DB_CONFIG.tableNames.archive} ORDER BY random() LIMIT ? ) - AND inactive = '' AND featured = 1 ORDER BY random() LIMIT ?`; - rowCountQuery = `SELECT COUNT(*) FROM ${DB_CONFIG.tableNames.archive} WHERE inactive = ''`; - } else { - // projects page - all archived projects data regardless if they are archived or not - projectsQuery = `SELECT * FROM ${DB_CONFIG.tableNames.archive} WHERE oid NOT IN - ( SELECT oid FROM ${DB_CONFIG.tableNames.archive} ORDER BY archive_id LIMIT ? ) - AND inactive = '' ORDER BY archive_id LIMIT ?`; - rowCountQuery = `SELECT COUNT(*) FROM ${DB_CONFIG.tableNames.archive} WHERE inactive = ''`; - } - const projectsPromise = db.query(projectsQuery, [skipNum, resultLimit]); - const rowCountPromise = db.query(rowCountQuery); - Promise.all([rowCountPromise, projectsPromise]) - .then(([[rowCount], projects]) => { - res.send({ - totalProjects: rowCount[Object.keys(rowCount)[0]], - projects: projects, - }); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - // endpoint for getting ALL archive data to view within admin view/editor - db_router.get("/getArchiveProjects", (req, res, next) => { - const { resultLimit, offset } = req.query; - let skipNum = offset * resultLimit; - let projectsQuery = `SELECT * FROM ${DB_CONFIG.tableNames.archive} WHERE - oid NOT IN (SELECT oid FROM ${DB_CONFIG.tableNames.archive} ORDER BY archive_id LIMIT ?) - ORDER BY archive_id LIMIT ?`; - let rowCountQuery = `SELECT COUNT(*) FROM ${DB_CONFIG.tableNames.archive}`; - - const projectsPromise = db.query(projectsQuery, [skipNum, resultLimit]); - const rowCountPromise = db.query(rowCountQuery); - - Promise.all([rowCountPromise, projectsPromise]) - .then(([[rowCount], projects]) => { - res.send({ - totalProjects: rowCount[Object.keys(rowCount)[0]], - projects: projects, - }); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - /** - * Responds with projects from database - * - * TODO: Add pagination - */ - db_router.get( - "/getProjects", - [UserAuth.isCoachOrAdmin], - async (req, res, next) => { - const query = "SELECT * from projects"; - db.query(query) - .then((projects) => res.send(projects)) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getCandidateProjects", - [UserAuth.isSignedIn], - async (req, res, next) => { - const query = - "SELECT * from projects WHERE projects.status = 'candidate';"; - db.query(query) - .then((projects) => res.send(projects)) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getMyProjects", - [UserAuth.isSignedIn], - async (req, res, next) => { - let query; - let params; - switch (req.user.type) { - case ROLES.COACH: - query = `SELECT projects.* - FROM projects - INNER JOIN project_coaches - ON (projects.project_id = project_coaches.project_id AND project_coaches.coach_id = ?);`; - params = [req.user.system_id]; - break; - case ROLES.STUDENT: - query = `SELECT users.system_id, users.semester_group, projects.* - FROM users - INNER JOIN projects - ON users.system_id = ? AND projects.project_id = users.project;`; - params = [req.user.system_id]; - break; - case ROLES.ADMIN: - query = - "SELECT * FROM projects WHERE projects.status NOT IN ('completed', 'rejected', 'archive');"; - params = []; - break; - default: - const error = new Error( - "Invalid user type...something must be very very broken...", - ); - error.statusCode = 500; - return next(error); - } - - db.query(query, params) - .then((proposals) => res.send(proposals)) - .catch((err) => res.status(500).send(err)); - }, - ); - - db_router.get( - "/getSemesterProjects", - [UserAuth.isSignedIn], - async (req, res, next) => { - let query; - let params; - switch (req.user.type) { - case ROLES.COACH: - query = ` - SELECT projects.* - FROM projects - WHERE projects.semester IN - (SELECT projects.semester - FROM projects - INNER JOIN project_coaches - ON (projects.project_id = project_coaches.project_id AND project_coaches.coach_id = ?)) - ;`; - params = [req.user.system_id]; - break; - case ROLES.STUDENT: - query = `SELECT users.system_id, projects.* - FROM users - INNER JOIN projects - ON users.system_id = ? AND projects.semester = users.semester_group;`; - params = [req.user.system_id]; - break; - case ROLES.ADMIN: - query = - "SELECT * FROM projects WHERE projects.status NOT IN ('in progress', 'completed', 'rejected', 'archive');"; - params = []; - break; - default: - const error = new Error( - "Invalid user type...something must be very very broken...", - ); - error.statusCode = 500; - return next(error); - } - - db.query(query, params) - .then((projects) => res.send(projects)) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/editArchive", - [UserAuth.isAdmin, UserAuth.canWrite], - async (req, res, next) => { - let body = req.body; - const updateArchiveQuery = `UPDATE ${DB_CONFIG.tableNames.archive} - SET featured=?, outstanding=?, creative=?, priority=?, - title=?, project_id=?, team_name=?, - members=?, sponsor=?, coach=?, - poster_thumb=?, poster_full=?, archive_image=?, synopsis=?, - video=?, name=?, dept=?, - start_date=?, end_date=?, keywords=?, url_slug=?, inactive=?, locked=? - WHERE archive_id = ?`; - const inactive = - body.inactive === "true" - ? moment().format(CONSTANTS.datetime_format) - : ""; - - const locked = - body.locked === "true" - ? req.user.fname + - " " + - req.user.lname + - " locked at " + - moment().format(CONSTANTS.datetime_format) - : ""; - - const checkBox = (data) => { - if (data === "true" || data === "1") { - return 1; - } - return 0; - }; - - const strToInt = (data) => { - if (typeof data === "string") { - return parseInt(data); - } - return 0; - }; - - let updateArchiveParams = [ - checkBox(body.featured), - checkBox(body.outstanding), - checkBox(body.creative), - strToInt(body.priority), - body.title, - body.project_id, - body.team_name, - body.members, - body.sponsor, - body.coach, - body.poster_thumb, - body.poster_full, - body.archive_image, - body.synopsis, - body.video, - body.name, - body.dept, - body.start_date, - body.end_date, - body.keywords, - body.url_slug, - inactive, - locked, - body.archive_id, - ]; - - db.query(updateArchiveQuery, updateArchiveParams) - .then(() => { - return res.status(200).send(); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/createArchive", - [UserAuth.isAdmin, UserAuth.canWrite], - body("featured") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - async (req, res, next) => { - let body = req.body; - const inactive = - body.inactive === "true" - ? moment().format(CONSTANTS.datetime_format) - : ""; - const locked = - body.locked === "true" - ? req.user.fname + - " " + - req.user.lname + - " locked at " + - moment().format(CONSTANTS.datetime_format) - : ""; - - const updateArchiveQuery = `INSERT INTO ${DB_CONFIG.tableNames.archive}(featured, outstanding, creative, - priority, title, project_id, team_name, members, sponsor, coach, poster_thumb, - poster_full, archive_image, synopsis, video, name, dept, start_date, end_date, - keywords, url_slug, inactive, locked) - VALUES(?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, ?, ?, ?);`; - - const checkBox = (data) => { - if (data === "true" || data === "1") { - return 1; - } - return 0; - }; - - const strToInt = (data) => { - if (typeof data === "string") { - return parseInt(data); - } - return 0; - }; - - const updateArchiveParams = [ - checkBox(body.featured), - checkBox(body.outstanding), - checkBox(body.creative), - strToInt(body.priority), - body.title, - body.project_id, - body.team_name, - body.members, - body.sponsor, - body.coach, - body.poster_thumb, - body.poster_full, - body.archive_image, - body.synopsis, - body.video, - body.name, - body.dept, - body.start_date, - body.end_date, - body.keywords, - body.url_slug, - inactive, - locked, - ]; - - db.query(updateArchiveQuery, updateArchiveParams) - .then((response) => { - return res.status(200).send(response); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/editArchiveStudent", - [UserAuth.isSignedIn, UserAuth.canWrite], - async (req, res, next) => { - let body = req.body; - let files = req.files; - const updateArchiveQuery = `UPDATE ${DB_CONFIG.tableNames.archive} - SET featured=?, outstanding=?, creative=?, priority=?, - title=?, project_id=?, team_name=?, - members=?, sponsor=?, coach=?, - poster_thumb=?, poster_full=?, archive_image=?, synopsis=?, - video=?, name=?, dept=?, - start_date=?, end_date=?, keywords=?, url_slug=?, inactive=?, locked=? - WHERE archive_id = ?`; - const inactive = - body.inactive === "true" - ? moment().format(CONSTANTS.datetime_format) - : ""; - - const locked = - body.locked === "true" - ? req.user.fname + - " " + - req.user.lname + - " locked at " + - moment().format(CONSTANTS.datetime_format) - : ""; - - let files_uploaded = []; - - let poster_full = ``; - let poster_thumb = ``; - let archive_image = ``; - let video = ``; - if (!(files === undefined || files === null)) { - if (files.poster_full === undefined) { - poster_full = body.poster_full; - poster_thumb = body.poster_thumb; - } else { - if ( - files.poster_full.mimetype == "image/png" && - files.poster_full.size <= 30000000 - ) { - files.poster_full.name = body.url_slug + "-poster"; - poster_full = `${files.poster_full.name}`; - poster_thumb = poster_full; - let poster_URL = path.join( - __dirname, - `../../resource/archivePosters`, - ); - files_uploaded.push([files.poster_full, poster_URL]); - } else { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - } - - if (files.archive_image === undefined) { - archive_image = body.archive_image; - } else { - if ( - files.archive_image.mimetype == "image/png" && - files.archive_image.size <= 30000000 - ) { - files.archive_image.name = body.url_slug + "-image"; - archive_image = `${files.archive_image.name}`; - let image_URL = path.join( - __dirname, - `../../resource/archiveImages`, - ); - files_uploaded.push([files.archive_image, image_URL]); - } else { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - } - - if (files.video === undefined) { - video = body.video; - } else { - if ( - files.video.mimetype == "video/mp4" && - files.video.size <= 300000000 - ) { - files.video.name = body.url_slug + "-video"; - video = `${files.video.name}`; - let video_URL = path.join( - __dirname, - `../../resource/archiveVideos`, - ); - files_uploaded.push([files.video, video_URL]); - } else { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - } - - for (let i = 0; i < files_uploaded.length; i++) { - fs.mkdirSync(files_uploaded[i][1], { recursive: true }); - files_uploaded[i][0].mv( - `${files_uploaded[i][1]}/${files_uploaded[i][0].name}`, - function (err) { - if (err) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - }, - ); - } - } else { - poster_full = body.poster_full; - poster_thumb = body.poster_thumb; - archive_image = body.archive_image; - video = body.video; - } - - const checkBox = (data) => { - if (data === "true" || data === "1") { - return 1; - } - return 0; - }; - - const strToInt = (data) => { - if (typeof data === "string") { - return parseInt(data); - } - return 0; - }; - - let updateArchiveParams = [ - checkBox(body.featured), - checkBox(body.outstanding), - checkBox(body.creative), - strToInt(body.priority), - body.title, - body.project_id, - body.team_name, - body.members, - body.sponsor, - body.coach, - poster_thumb, - poster_full, - archive_image, - body.synopsis, - video, - body.name, - body.dept, - body.start_date, - body.end_date, - body.keywords, - body.url_slug, - inactive, - locked, - body.archive_id, - ]; - - db.query(updateArchiveQuery, updateArchiveParams) - .then(() => { - return res.status(200).send(); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/createArchiveStudent", - [UserAuth.isSignedIn, UserAuth.canWrite], - body("featured") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - async (req, res, next) => { - let body = req.body; - const inactive = - body.inactive === "true" - ? moment().format(CONSTANTS.datetime_format) - : ""; - const locked = - body.locked === "true" - ? req.user.fname + - " " + - req.user.lname + - " locked at " + - moment().format(CONSTANTS.datetime_format) - : ""; - - const name = body.url_slug; //this value needs to be unique, but isn't used, so this is a relatively safe method. - - let files = req.files; - let files_uploaded = []; - - let poster_full = ``; - let poster_thumb = ``; - let archive_image = ``; - let video = ``; - if (!(files === undefined || files === null)) { - if (files.poster_full === undefined) { - poster_full = body.poster_full; - poster_thumb = body.poster_thumb; - } else { - if ( - files.poster_full.mimetype == "image/png" && - files.poster_full.size <= 30000000 - ) { - files.poster_full.name = body.url_slug + "-poster"; - poster_full = `${files.poster_full.name}`; - poster_thumb = poster_full; - let poster_URL = path.join( - __dirname, - `../../resource/archivePosters`, - ); - files_uploaded.push([files.poster_full, poster_URL]); - } else { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - } - - if (files.archive_image === undefined) { - archive_image = body.archive_image; - } else { - if ( - files.archive_image.mimetype == "image/png" && - files.archive_image.size <= 30000000 - ) { - files.archive_image.name = body.url_slug + "-image"; - archive_image = `${files.archive_image.name}`; - let image_URL = path.join( - __dirname, - `../../resource/archiveImages`, - ); - files_uploaded.push([files.archive_image, image_URL]); - } else { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - } - - if (files.video === undefined) { - video = body.video; - } else { - if ( - files.video.mimetype == "video/mp4" && - files.video.size <= 300000000 - ) { - files.video.name = body.url_slug + "-video"; - video = `${files.video.name}`; - let video_URL = path.join( - __dirname, - `../../resource/archiveVideos`, - ); - files_uploaded.push([files.video, video_URL]); - } else { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - } - - for (let i = 0; i < files_uploaded.length; i++) { - fs.mkdirSync(files_uploaded[i][1], { recursive: true }); - if ( - fs.existsSync( - `${files_uploaded[i][1]}/${files_uploaded[i][0].name}`, - ) - ) { - fs.unlink(`${files_uploaded[i][1]}/${files_uploaded[i][0].name}`); - } - files_uploaded[i][0].mv( - `${files_uploaded[i][1]}/${files_uploaded[i][0].name}`, - function (err) { - if (err) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - }, - ); - } - } else { - poster_full = body.poster_full; - poster_thumb = body.poster_thumb; - archive_image = body.archive_image; - video = body.video; - } - - const updateArchiveQuery = `INSERT INTO ${DB_CONFIG.tableNames.archive}(featured, outstanding, creative, - priority, title, project_id, team_name, members, sponsor, coach, poster_thumb, - poster_full, archive_image, synopsis, video, name, dept, start_date, end_date, - keywords, url_slug, inactive, locked) - VALUES(?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, ?, ?, ?);`; - - const checkBox = (data) => { - if (data === "true" || data === "1") { - return 1; - } - return 0; - }; - - const strToInt = (data) => { - if (typeof data === "string") { - return parseInt(data); - } - return 0; - }; - - const updateArchiveParams = [ - checkBox(body.featured), - checkBox(body.outstanding), - checkBox(body.creative), - strToInt(body.priority), - body.title, - body.project_id, - body.team_name, - body.members, - body.sponsor, - body.coach, - poster_thumb, - poster_full, - archive_image, - body.synopsis, - video, - name, - body.dept, - body.start_date, - body.end_date, - body.keywords, - body.url_slug, - inactive, - locked, - ]; - - db.query(updateArchiveQuery, updateArchiveParams) - .then((response) => { - return res.status(200).send(response); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - //Gets the start and end dates of a project based on the semester that it is associated with. - db_router.get("/getProjectDates", UserAuth.isSignedIn, (req, res, next) => { - const getDatesQuery = `SELECT start_date, end_date FROM semester_group WHERE semester_id = ?`; - const getDatesParams = [req.query.semester]; - db.query(getDatesQuery, getDatesParams) - .then((dates) => { - res.send(dates); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.post( - "/editProject", - [ - UserAuth.isAdmin, - UserAuth.canWrite, - // TODO: Should the max length be set to something smaller than 5000? - body("title") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("organization") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("primary_contact") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("contact_email") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("contact_phone") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("background_info") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("project_description") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("project_scope") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("project_challenges") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("constraints_assumptions") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("sponsor_provided_resources") - .trim() - .escape() - .isLength({ max: 5000 }), - body("project_search_keywords").trim().escape().isLength({ max: 5000 }), - body("sponsor_deliverables") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("proprietary_info").trim().escape().isLength({ max: 5000 }), - body("sponsor_alternate_time").trim().escape().isLength({ max: 5000 }), - body("sponsor_avail_checked") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("project_agreements_checked") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("assignment_of_rights") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - - body("team_name") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("poster") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("video") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("website") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("synopsis") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("sponsor") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("semester") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - // body("date").not().isEmpty().trim().escape().withMessage("Cannot be empty"), - body("projectCoaches").trim().escape().isLength({ max: 5000 }), - ], - async (req, res, next) => { - let body = req.body; - - const updateProjectSql = `UPDATE ${DB_CONFIG.tableNames.senior_projects} - SET status=?, title=?, display_name=?, organization=?, primary_contact=?, contact_email=?, contact_phone=?, - background_info=?, project_description=?, project_scope=?, project_challenges=?, - sponsor_provided_resources=?, project_search_keywords=?, constraints_assumptions=?, sponsor_deliverables=?, - proprietary_info=?, sponsor_alternate_time=?, sponsor_avail_checked=?, project_agreements_checked=?, assignment_of_rights=?, - team_name=?, poster=?, video=?, website=?, synopsis=?, sponsor=?, semester=? - WHERE project_id = ?`; - - const updateProjectParams = [ - body.status, - body.title, - body.display_name ? body.display_name : null, // Empty strings should be turned to null - body.organization, - body.primary_contact, - body.contact_email, - body.contact_phone, - body.background_info, - body.project_description, - body.project_scope, - body.project_challenges, - body.sponsor_provided_resources, - body.project_search_keywords, - body.constraints_assumptions, - body.sponsor_deliverables, - body.proprietary_info, - body.sponsor_alternate_time, - body.sponsor_avail_checked, - body.project_agreements_checked, - body.assignment_of_rights, - body.team_name, - body.poster, - body.video, - body.website, - body.synopsis, - body.sponsor, - body.semester || null, - body.project_id, - ]; - - const insertValues = body.projectCoaches - .split(",") - .map((coachId) => ` ('${body.project_id}', '${coachId}')`); - const deleteValues = body.projectCoaches - .split(",") - .map((coachId) => `'${coachId}'`); - const updateCoachesSql = `INSERT OR IGNORE INTO '${DB_CONFIG.tableNames.project_coaches}' ('project_id', 'coach_id') VALUES ${insertValues};`; - const deleteCoachesSQL = `DELETE FROM '${DB_CONFIG.tableNames.project_coaches}' - WHERE project_coaches.project_id = '${body.project_id}' - AND project_coaches.coach_id NOT IN (${deleteValues});`; - - Promise.all([ - db.query(updateProjectSql, updateProjectParams), - db.query(updateCoachesSql), - db.query(deleteCoachesSQL), - ]) - .then((values) => { - return res.sendStatus(200); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - /** - * Updates a proposal with the given information - */ - db_router.patch( - "/updateProposalStatus", - [ - UserAuth.isAdmin, - UserAuth.canWrite, - body("*").trim().escape().isJSON().isAlphanumeric(), - ], - (req, res, next) => { - const query = `UPDATE ${DB_CONFIG.tableNames.senior_projects} SET status = ? WHERE project_id = ?`; - db.query(query, [req.body.status, req.body.project_id]) - .then(() => { - res.sendStatus(200); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - /** - * Responds with a list of links to pdf versions of proposal forms - * - * NOTE: This route is unused and untested. - */ - db_router.get( - "/getProposalPdfNames", - UserAuth.isSignedIn, - (req, res, next) => { - fs.readdir( - path.join(__dirname, "../proposal_docs"), - function (err, files) { - if (err) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - let fileLinks = []; - files.forEach(function (file) { - fileLinks.push(file.toString()); - }); - - res.send(fileLinks); - }, - ); - }, - ); - - db_router.get("/getProposalPdf", UserAuth.isSignedIn, (req, res) => { - if (req.query.project_id) { - let projectId = req.query.project_id.replace(/\\|\//g, ""); // attempt to avoid any path traversal issues - res.sendFile(path.join(__dirname, `../proposal_docs/${projectId}.pdf`)); - } else res.send("File not found"); - }); - - // NOTE: This route is unused and untested. - db_router.get( - "/getProposalAttachmentNames", - UserAuth.isSignedIn, - (req, res, next) => { - if (req.query.project_id) { - let projectId = req.query.project_id.replace(/\\|\//g, ""); // attempt to avoid any path traversal issues, get the name with no extension - fs.readdir( - path.join(__dirname, `./server/sponsor_proposal_files/${projectId}`), - function (err, files) { - if (err) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - let fileLinks = []; - files.forEach(function (file) { - fileLinks.push(file.toString()); - }); - - res.send(fileLinks); - }, - ); - } else { - res.status(404).send("Bad request"); - } - }, - ); - - db_router.get("/getProposalAttachment", UserAuth.isSignedIn, (req, res) => { - if (req.query.project_id && req.query.name) { - let projectId = req.query.project_id.replace(/\\|\//g, ""); // attempt to avoid any path traversal issues - let name = req.query.name.replace(/\\|\//g, ""); // attempt to avoid any path traversal issues - res.sendFile( - path.join(__dirname, `../sponsor_proposal_files/${projectId}/${name}`), - ); - } else res.send("File not found"); - }); - - /* - * Route to get sponsor data, particularly for getting all sponsor - * emails for messaging. Sent to admin sponsor tab for building a csv - */ - db_router.get("/getSponsorData", UserAuth.isAdmin, (req, res, next) => { - let query = `SELECT * FROM sponsors WHERE inActive = 0 AND doNotEmail = 0`; - let params = []; - db.query(query, params) - .then((response) => { - res.send(response); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - /** - * WARN: THIS IS VERY DANGEROUS AND IT CAN BE USED TO OVERWRITE SERVER FILES. - */ - db_router.post( - "/uploadFiles", - [UserAuth.isAdmin, UserAuth.canWrite], - (req, res, next) => { - let filesUploaded = []; - - // Attachment Handling - if (req.files && req.files.files) { - // If there is only one attachment, then it does not come as a list - if (req.files.files.length === undefined) { - req.files.files = [req.files.files]; - } - - const formattedPath = `resource/${req.body.path}`; - const baseURL = path.join(__dirname, `../../${formattedPath}`); - - //If directory, exists, it won't make one, otherwise it will based on the baseUrl :/ - fs.mkdirSync(baseURL, { recursive: true }); - for (let x = 0; x < req.files.files.length; x++) { - req.files.files[x].mv( - `${baseURL}/${req.files.files[x].name}`, - function (err) { - if (err) { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - }, - ); - filesUploaded.push( - `${process.env.BASE_URL}/${formattedPath}/${req.files.files[x].name}`, - ); - } - } - res.send({ msg: "Success!", filesUploaded: filesUploaded }); - }, - ); - - /** - * WARN: THIS IS VERY DANGEROUS AND IT CAN BE USED TO OVERWRITE SERVER FILES. - */ - db_router.post("/uploadFilesStudent", UserAuth.canWrite, (req, res, next) => { - let filesUploaded = []; - - // Attachment Handling - if (req.files && req.files.files) { - // If there is only one attachment, then it does not come as a list - if (req.files.files.length === undefined) { - req.files.files = [req.files.files]; - } - - const formattedPath = `resource/${req.body.path}`; - const baseURL = path.join(__dirname, `../../${formattedPath}`); - - //If directory, exists, it won't make one, otherwise it will based on the baseUrl :/ - fs.mkdirSync(baseURL, { recursive: true }); - for (let x = 0; x < req.files.files.length; x++) { - req.files.files[x].mv( - `${baseURL}/${req.files.files[x].name}`, - function (err) { - if (err) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - }, - ); - filesUploaded.push( - `${process.env.BASE_URL}/${formattedPath}/${req.files.files[x].name}`, - ); - let fileName = req.files.files[x].name; - let pathString = req.body.path; - pathString = pathString.split("/"); - pathString.shift(); - pathString = '"' + pathString.join("/") + "/" + fileName + '"'; - let query = `UPDATE ${DB_CONFIG.tableNames.archive} - SET ${req.body.column} = ${pathString} - WHERE archive_id = ${req.body.archive}`; - db.query(query).catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - } - } - res.send({ msg: "Success!", filesUploaded: filesUploaded }); - }); - - db_router.post( - "/createDirectory", - [UserAuth.isAdmin, UserAuth.canWrite], - (req, res, next) => { - const formattedPath = - req.query.path === "" ? `resource/` : `resource/${req.query.path}`; - const baseURL = path.join(__dirname, `../../${formattedPath}`); - if (!fs.existsSync(baseURL)) { - fs.mkdirSync(baseURL, { recursive: true }); - res.send({ msg: "Success!" }); - } else { - const error = new Error("Directory already exists"); - error.statusCode = 500; - return next(error); - } - }, - ); - - db_router.post( - "/renameDirectoryOrFile", - [UserAuth.isAdmin, UserAuth.canWrite], - (req, res, next) => { - const { oldPath, newPath } = req.query; - const formattedOldPath = - oldPath === "" ? `resource/` : `resource/${oldPath}`; - const formattedNewPath = - newPath === "" ? `resource/` : `resource/${newPath}`; - const baseURLOld = path.join(__dirname, `../../${formattedOldPath}`); - const baseURLNew = path.join(__dirname, `../../${formattedNewPath}`); - - // New path already exists, so we can't rename - if (fs.existsSync(baseURLNew)) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - // Copy all files from old directory to new directory - if (fs.lstatSync(baseURLOld).isDirectory()) { - fse.copySync(baseURLOld, baseURLNew); - fs.rmdirSync(baseURLOld, { recursive: true }); - res.send({ msg: "Success!" }); - // Rename file - } else if (fs.lstatSync(baseURLOld).isFile()) { - fs.renameSync(baseURLOld, baseURLNew); - res.send({ msg: "Success!" }); - } - }, - ); - - db_router.get("/getFiles", UserAuth.isAdmin, (req, res, next) => { - let fileData = []; - // This is the path with the specified directory we want to find files in. - const formattedPath = - req.query.path === "" ? `resource/` : `resource/${req.query.path}`; - const baseURL = path.join(__dirname, `../../${formattedPath}`); - - if (fs.existsSync(baseURL)) { - // Get the files in the directory - fs.readdir(baseURL, function (err, files) { - if (err) { - console.error(`Error reading directory ${baseURL}:`, err); - // Return empty array instead of throwing error for missing directories - res.send(fileData); - return; - } - - try { - const info = fs.statSync(baseURL); - files.forEach(function (file) { - try { - // Only files have sizes, directories do not. Send file size if it is a file - const fileInfo = fs.statSync(path.join(baseURL, file)); - if (fileInfo.isFile()) { - fileData.push({ - file: file, - size: fileInfo.size, - lastModified: fileInfo.ctime, - }); - } else { - fileData.push({ - file: file, - size: 0, - lastModified: info.ctime, - }); - } - } catch (fileErr) { - console.error(`Error processing file ${file}:`, fileErr); - // Skip files that can't be processed - } - }); - } catch (statErr) { - console.error( - `Error getting directory stats for ${baseURL}:`, - statErr, - ); - } - - res.send(fileData); - }); - } else { - console.log(`Directory does not exist: ${baseURL}`); - res.send(fileData); - } - }); - - db_router.get("/getProjectFiles", (req, res, next) => { - let fileData = []; - // This is the path with the specified directory we want to find files in. - const formattedPath = `resource/`; - const baseURL = path.join(__dirname, `../../${formattedPath}`); - fs.mkdirSync(baseURL, { recursive: true }); - // Get the files in the directory - fs.readdir(baseURL, function (err, files) { - if (err) { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - const info = fs.statSync(baseURL); - files.forEach(function (file) { - // Only files have sizes, directories do not. Send file size if it is a file - const fileInfo = fs.statSync(baseURL + file); - if (fileInfo.isFile()) { - fileData.push({ - file: file, - size: fileInfo.size, - lastModified: fileInfo.ctime, - }); - } else { - fileData.push({ - file: file, - size: 0, - lastModified: info.ctime, - }); - } - }); - res.send(fileData); - }); - }); - - db_router.get("/getProjectFiles", (req, res, next) => { - let fileData = []; - // This is the path with the specified directory we want to find files in. - const formattedPath = `resource/`; - const baseURL = path.join(__dirname, `../../${formattedPath}`); - fs.mkdirSync(baseURL, { recursive: true }); - // Get the files in the directory - fs.readdir(baseURL, function (err, files) { - if (err) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - const info = fs.statSync(baseURL); - files.forEach(function (file) { - // Only files have sizes, directories do not. Send file size if it is a file - const fileInfo = fs.statSync(baseURL + file); - if (fileInfo.isFile()) { - fileData.push({ - file: file, - size: fileInfo.size, - lastModified: fileInfo.ctime, - }); - } else { - fileData.push({ - file: file, - size: 0, - lastModified: info.ctime, - }); - } - }); - res.send(fileData); - }); - }); - - db_router.delete( - "/removeFile", - [UserAuth.isAdmin, UserAuth.canWrite], - (req, res, next) => { - const formattedPath = `resource/${req.query.path}`; - const baseURL = path.join(__dirname, `../../${formattedPath}`); - fs.unlink(baseURL, (err) => { - if (err) { - const error = new Error(err); - error.statusCode = 500; - return next(error); - } else { - res.send({ msg: "Success!" }); - } - }); - }, - ); - - db_router.delete( - "/removeDirectory", - [UserAuth.isAdmin, UserAuth.canWrite], - (req, res, next) => { - const formattedPath = `resource/${req.query.path}`; - const baseURL = path.join(__dirname, `../../${formattedPath}`); - if (fs.existsSync(baseURL)) { - fs.rmdirSync(baseURL, { recursive: true }); - return res.status(200).send({ msg: "Success!" }); - } else { - const error = new Error("Directory does not exist, cannot delete"); - error.statusCode = 500; - return next(error); - } - }, - ); - - db_router.post( - "/submitProposal", - [ - // TODO: Should the max length be set to something smaller than 5000? - body("title") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }) - .withMessage("Title must be under 50 characters"), - body("organization") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("primary_contact") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("contact_email") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("contact_phone") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("background_info") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("project_description") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("project_scope") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("project_challenges") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("sponsor_provided_resources") - .trim() - .escape() - .isLength({ max: 5000 }), - body("constraints_assumptions") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("sponsor_deliverables") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - body("proprietary_info").trim().escape().isLength({ max: 5000 }), - body("sponsor_alternate_time").trim().escape().isLength({ max: 5000 }), - body("sponsor_avail_checked") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("project_agreements_checked") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty"), - body("assignment_of_rights") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 5000 }), - ], - async (req, res, next) => { - let result = validationResult(req); - - if (result.errors.length !== 0) { - const errorMessages = result.errors - .map((error) => `${error.param}: ${error.msg}`) - .join(", "); - const error = new Error(`Validation failed: ${errorMessages}`); - error.statusCode = 400; - return next(error); - } - - // Insert into the database - let body = req.body; - - let date = new Date(); - let timeString = `${date.getFullYear()}-${date.getUTCMonth()}-${date.getDate()}`; - const projectId = `${timeString}_${nanoid()}`; - - let filenamesCSV = ""; - // Attachment Handling - if (req.files && req.files.attachments) { - // If there is only one attachment, then it does not come as a list - if (req.files.attachments.length === undefined) { - req.files.attachments = [req.files.attachments]; - } - - if (req.files.attachments.length > 5) { - // Don't allow more than 5 files - const error = new Error("Maximum of 5 files allowed"); - error.statusCode = 400; - return next(error); - } - - const baseURL = path.join( - __dirname, - `../sponsor_proposal_files/${projectId}`, - ); - - fs.mkdirSync(baseURL, { recursive: true }); - - for (let x = 0; x < req.files.attachments.length; x++) { - if (req.files.attachments[x].size > 15 * 1024 * 1024) { - // 15mb limit exceeded - const error = new Error("File size limit exceeded"); - error.statusCode = 400; - return next(error); - } - if ( - !CONFIG.accepted_file_types.includes( - path.extname(req.files.attachments[x].name), - ) - ) { - // send an error if the file is not an accepted type - const error = new Error("file type not accepted"); - error.statusCode = 400; - return next(error); - } - - // Append the file name to the CSV string, begin with a comma if x is not 0 - filenamesCSV += - x === 0 - ? `${req.files.attachments[x].name}` - : `, ${req.files.attachments[x].name}`; - - req.files.attachments[x].mv( - `${baseURL}/${req.files.attachments[x].name}`, - function (err) { - if (err) { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - }, - ); - } - } - const sql = `INSERT INTO ${DB_CONFIG.tableNames.senior_projects} - (project_id, status, title, organization, primary_contact, contact_email, contact_phone, attachments, - background_info, project_description, project_scope, project_challenges, - sponsor_provided_resources, constraints_assumptions, sponsor_deliverables, - proprietary_info, sponsor_alternate_time, sponsor_avail_checked, project_agreements_checked, assignment_of_rights) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`; - - const params = [ - projectId, - "submitted", - body.title.substring(0, 50), - body.organization, - body.primary_contact, - body.contact_email, - body.contact_phone, - filenamesCSV, - body.background_info, - body.project_description, - body.project_scope, - body.project_challenges, - body.sponsor_provided_resources, - body.constraints_assumptions, - body.sponsor_deliverables, - body.proprietary_info, - body.sponsor_alternate_time, - body.sponsor_avail_checked, - body.project_agreements_checked, - body.assignment_of_rights, - ]; - - db.query(sql, params) - .then(() => { - let doc = new PDFDoc(); - const baseURL = path.join(__dirname, `../proposal_docs/`); - fs.mkdirSync(baseURL, { recursive: true }); - doc.pipe(fs.createWriteStream(`${baseURL}/${projectId}.pdf`)); - - doc.font("Times-Roman"); - - for (let key of Object.keys(DB_CONFIG.senior_project_proposal_keys)) { - doc - .fill("blue") - .fontSize(16) - .text(DB_CONFIG.senior_project_proposal_keys[key]), - { - underline: true, - }; - doc - .fontSize(12) - .fill("black") - .text(convert(he.decode(body[key] || ""))); // Text value from proposal - doc.moveDown(); - doc.save(); - } - - doc.fill("blue").fontSize(16).text("Attachments"), - { - underline: true, - }; - doc.fontSize(12).fill("black").text(filenamesCSV); - doc.moveDown(); - doc.save(); - - doc.end(); - return res.status(200).send(); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get("/getArchivePoster", (req, res) => { - res.sendFile( - path.join( - __dirname, - "../../resource/archivePosters/" + req.query.fileName, - ), - ); - }); - - db_router.get("/getArchiveVideo", (req, res) => { - res.sendFile( - path.join( - __dirname, - "../../resource/archiveVideos/" + req.query.fileName, - ), - ); - }); - - db_router.get("/getArchiveImage", (req, res) => { - res.sendFile( - path.join( - __dirname, - "../../resource/archiveImages/" + req.query.fileName, - ), - ); - }); - - db_router.get( - "/getActiveTimelines", - [UserAuth.isSignedIn], - (req, res, next) => { - calculateActiveTimelines(req.user) - .then((timelines) => { - res.json(timelines); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/submitAction", - [UserAuth.isSignedIn, UserAuth.canWrite, body("*").trim()], - async (req, res, next) => { - let result = validationResult(req); - - if (result.errors.length !== 0) { - return res.status(400).send("Input is invalid"); - } - - let body = req.body; - - const query = `SELECT * FROM actions WHERE action_id = ?;`; - const [action] = await db.query(query, [body.action_template]); - - const startDate = new Date(action.start_date); - if (startDate > Date.now()) { - const error = new Error("Action start date is in the future"); - error.statusCode = 400; - return next(error); - } - - switch (action.action_target) { - case ACTION_TARGETS.ADMIN: - if (req.user.type !== ROLES.ADMIN) { - const error = new Error("Only admins can submit admin actions"); - error.statusCode = 401; - return next(error); - } - break; - case ACTION_TARGETS.COACH: - if (req.user.type !== ROLES.COACH && req.user.type !== ROLES.ADMIN) { - const error = new Error("Only coaches can submit coach actions"); - error.statusCode = 401; - return next(error); - } - break; - case ACTION_TARGETS.INDIVIDUAL: - if (req.user.type !== ROLES.STUDENT) { - const error = new Error( - "Only students can submit individual actions", - ); - error.statusCode = 401; - return next(error); - } - break; - //TODO: Add case for PEER_EVALUATION - case ACTION_TARGETS.PEER_EVALUATION: - if ( - req.user.type !== ROLES.COACH && - req.user.type !== ROLES.STUDENT - ) { - const error = new Error( - "Only coaches and students can submit peer evaluations", - ); - error.statusCode = 401; - return next(error); - } - break; - case ACTION_TARGETS.COACH_ANNOUNCEMENT: - case ACTION_TARGETS.STUDENT_ANNOUNCEMENT: - const error = new Error("You cannot submit an announcement"); - error.statusCode = 401; - return next(error); - case ACTION_TARGETS.TEAM: - // Anyone can submit team actions - break; - case ACTION_TARGETS.PEER_EVALUATION: - if (req.user.type !== ROLES.STUDENT) { - const error = new Error( - "Only students can submit peer evaluations.", - ); - error.statusCode = 401; - return next(error); - } - break; - default: - error = new Error("Invalid action target"); - error.statusCode = 400; - return next(error); - } - - let date = new Date(); - let timeString = `${date.getFullYear()}-${date.getUTCMonth()}-${date.getDate()}`; - const submission = `${timeString}_${nanoid()}`; - - let baseURL = path.join( - __dirname, - `../project_docs/${body.project}/${action.action_target}/${action.action_id}/${req.user.system_id}/${submission}`, - ); - - // Attachment Handling - let filenamesCSV = ""; - if (req.files && req.files.attachments) { - // If there is only one attachment, then it does not come as a list - if (req.files.attachments.length === undefined) { - req.files.attachments = [req.files.attachments]; - } - - if (req.files.attachments.length > 5) { - // Don't allow more than 5 files - const error = new Error("Maximum of 5 files allowed"); - error.statusCode = 400; - return next(error); - } - - fs.mkdirSync(baseURL, { recursive: true }); - - for (let x = 0; x < req.files.attachments.length; x++) { - if ( - req.files.attachments[x].size > - (action.file_size || defaultFileSizeLimit) - ) { - // 15mb limit exceeded - const responseText = - "File exceeded submission size limit of: " + - humanFileSize(action.file_size || defaultFileSizeLimit, false, 0); - const error = new Error(responseText); - error.statusCode = 400; - return next(error); - } - if ( - !action.file_types - .split(",") - .includes( - path.extname(req.files.attachments[x].name).toLocaleLowerCase(), - ) - ) { - // send an error if the file is not an accepted type - const error = new Error("file type not accepted"); - error.statusCode = 400; - return next(error); - } - - // Append the file name to the CSV string, begin with a comma if x is not 0 - filenamesCSV += - x === 0 - ? `${submission}/${req.files.attachments[x].name}` - : `,${submission}/${req.files.attachments[x].name}`; - - req.files.attachments[x].mv( - `${baseURL}/${req.files.attachments[x].name}`, - function (err) { - if (err) { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - } - }, - ); - } - } - - let insertAction = ` - INSERT INTO action_log( - action_template, - system_id, - project, - form_data, - files - ${(req.user.mock && ",mock_id") || ""} - ) - VALUES (?,?,?,?,?${(req.user.mock && ",?") || ""}) - `; - - let params = [ - body.action_template, - req.user.system_id, - body.project, - body.form_data, - filenamesCSV, - ]; - if (req.user.mock) { - params.push(req.user.mock.system_id); - } - - db.query(insertAction, params) - .then(() => { - return res.sendStatus(200); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get("/getHtml", (req, res, next) => { - let getHtmlQuery = `SELECT * FROM page_html`; - let queryParams = []; - //If there is a query parameter, then select html from specified table. - if (typeof req.query.name !== "undefined" && req.query.name) { - getHtmlQuery = `SELECT html FROM page_html WHERE name = ?`; - queryParams = [req.query.name]; - } - db.query(getHtmlQuery, queryParams) - .then((html) => { - // Replace placeholder with actual server base URL - const serverBaseUrl = - process.env.NODE_ENV === "production" - ? process.env.PRODUCTION_SERVER_URL || - process.env.BASE_URL || - `${req.protocol}://${req.get("host")}` - : `${req.protocol}://${req.get("host")}`; - - // Process HTML to replace placeholders - if (Array.isArray(html)) { - html = html.map((item) => { - if (item.html) { - item.html = item.html.replace( - /__SERVER_BASE_URL__/g, - serverBaseUrl, - ); - } - return item; - }); - } else if (html && html.html) { - html.html = html.html.replace(/__SERVER_BASE_URL__/g, serverBaseUrl); - } - - res.send(html); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.post( - "/editPage", - [UserAuth.isAdmin, UserAuth.canWrite], - (req, res, next) => { - let editPageQuery = `UPDATE page_html - SET html = ? - WHERE name = ? - `; - let promises = []; - //Individually update all html tables from the body of the req. - Object.keys(req.body).forEach((key) => { - let queryParams = [req.body[key], key]; - promises.push( - db - .query(editPageQuery, queryParams) - .then(() => { - //do nothing - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }), - ); - }); - Promise.all(promises).then(() => { - res.send({ msg: "Success!" }); - }); - }, - ); - - db_router.get("/getActions", [UserAuth.isAdmin], (req, res, next) => { - let getActionsQuery = ` - SELECT * - FROM actions - ORDER BY action_id desc - `; - db.query(getActionsQuery) - .then((values) => { - res.send(values); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.get( - "/getTimelineActions", - [UserAuth.isSignedIn], - async (req, res, next) => { - if ( - req.user.type === ROLES.STUDENT && - req.query.project_id !== req.user.project - ) { - const error = new Error("trying to acces project that is not yours"); - error.statusCode = 401; - return next(error); - } - - // Add a case for when the action target is 'peer_evaluation' - // The action is not done unless compelted by all students, AND the coach has passed it through - // - For better UI/UX, if the coach has not passed it through and the students have; the peer evaluation visual on the dashboard should be red INSTEAD of directly falling onto the UNHANDLED-CASE - let getTimelineActions = `SELECT action_title, action_id, start_date, due_date, semester, action_target, date_deleted, short_desc, file_types, file_size, page_html, - CASE - WHEN action_target IS 'admin' AND system_id IS NOT NULL THEN 'green' - WHEN action_target IS 'coach' AND system_id IS NOT NULL THEN 'green' - WHEN action_target IS 'team' AND system_id IS NOT NULL THEN 'green' - WHEN action_target = 'peer_evaluation' AND COUNT(DISTINCT system_id) IS (SELECT COUNT(DISTINCT system_id) FROM users WHERE users.project = ?) + 1 THEN 'green' - WHEN action_target = 'peer_evaluation' THEN 'red' - WHEN action_target IS 'individual' AND COUNT(DISTINCT system_id) IS (SELECT COUNT(DISTINCT system_id) FROM users WHERE users.project = ?) THEN 'green' - WHEN start_date <= date('now') AND due_date >= date('now') THEN 'yellow' - WHEN date('now') > due_date AND system_id IS NULL THEN 'red' - WHEN date('now') > due_date AND action_target IS 'individual' AND COUNT(DISTINCT system_id) != (SELECT COUNT(DISTINCT system_id) FROM users WHERE users.project = ?) THEN 'red' - WHEN date('now') < start_date THEN 'grey' - ELSE 'UNHANDLED-CASE' - END AS 'state' - FROM actions - LEFT JOIN action_log - ON action_log.action_template = actions.action_id AND action_log.project = ? - WHERE actions.date_deleted = '' AND actions.semester = (SELECT distinct projects.semester FROM projects WHERE projects.project_id = ?) - AND actions.action_target NOT IN ('${ACTION_TARGETS.COACH_ANNOUNCEMENT}', '${ACTION_TARGETS.STUDENT_ANNOUNCEMENT}') - GROUP BY actions.action_id`; - - db.query(getTimelineActions, [ - req.query.project_id, - req.query.project_id, - req.query.project_id, - req.query.project_id, - req.query.project_id, - ]) - .then((values) => { - res.send(values); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - /* - * This will join between the action and action_log tables. It returns the due date from - * a specific log_id's action_template # = action_id. - */ - db_router.get( - "/getLateSubmission", - [UserAuth.isSignedIn], - (req, res, next) => { - let getLateSubmissionQuery = `SELECT actions.due_date - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - WHERE action_log.action_log_id = ?`; - let params = [req.query.log_id]; - db.query(getLateSubmissionQuery, params) - .then((values) => { - res.send(values); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get("/getActionLogs", [UserAuth.isSignedIn], (req, res, next) => { - let getActionLogQuery = ""; - let params = []; - - switch (req.user.type) { - case ROLES.STUDENT: - // NOTE: Technically, users are able to see if coaches submitted actions to other projects, but they should not be able to see the actual submission content form this query so that should be fine - // This is because of the "OR users.type = '${ROLES.COACH}'" part of the following query. - getActionLogQuery = `SELECT action_log.action_log_id, action_log.submission_datetime, action_log.action_template, action_log.system_id, action_log.mock_id, action_log.project, - actions.action_title, actions.due_date, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) mock_name, - (SELECT users.type FROM users WHERE users.system_id = action_log.system_id) AS user_type, - (SELECT users.type FROM users WHERE users.system_id = action_log.mock_id) AS mock_type - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - WHERE action_log.action_template = ? AND action_log.project = ?`; - params = [req.query.action_id, req.user.project]; - break; - case ROLES.COACH: - case ROLES.ADMIN: - getActionLogQuery = `SELECT action_log.*, actions.action_title, actions.due_date, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) AS name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) AS mock_name, - (SELECT users.type FROM users WHERE users.system_id = action_log.system_id) AS user_type, - (SELECT users.type FROM users WHERE users.system_id = action_log.mock_id) AS mock_type - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - WHERE action_log.action_template = ? AND action_log.project = ?`; - params = [req.query.action_id, req.query.project_id]; - break; - - default: - const error = new Error("Unknown Role"); - error.statusCode = 401; - return next(error); - } - db.query(getActionLogQuery, params) - .then((values) => { - res.send(values); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.get( - "/getAllActionLogs", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { resultLimit, offset } = req.query; - - let getActionLogQuery = ""; - let queryParams = []; - let getActionLogCount = ""; - let countParams = []; - - switch (req.user.type) { - case ROLES.STUDENT: - getActionLogQuery = `SELECT action_log.action_log_id, action_log.submission_datetime AS submission_datetime, action_log.action_template, action_log.system_id, action_log.mock_id, action_log.project, - actions.action_target, actions.action_title, actions.semester, - projects.display_name, projects.title, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) mock_name - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - JOIN projects ON projects.project_id = action_log.project - WHERE action_log.project = ? - ORDER BY submission_datetime DESC - LIMIT ? OFFSET ?`; - queryParams = [req.user.project, resultLimit, offset * resultLimit]; - getActionLogCount = `SELECT COUNT(*) FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - WHERE action_log.project = ? - AND action_log.system_id in (SELECT users.system_id FROM users WHERE users.project = ?)`; - countParams = [req.user.project, req.user.project]; - break; - case ROLES.COACH: - getActionLogQuery = `SELECT action_log.action_log_id, action_log.submission_datetime AS submission_datetime, action_log.action_template, action_log.system_id, action_log.mock_id, action_log.project, - actions.action_target, actions.action_title, actions.semester, - projects.display_name, projects.title, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) mock_name - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - JOIN projects ON projects.project_id = action_log.project - WHERE action_log.project IN (SELECT project_id FROM project_coaches WHERE coach_id = ?) - ORDER BY submission_datetime DESC - LIMIT ? OFFSET ?`; - queryParams = [req.user.system_id, resultLimit, offset * resultLimit]; - getActionLogCount = `SELECT COUNT(*) FROM action_log WHERE action_log.project IN (SELECT project_id FROM project_coaches WHERE coach_id = ?)`; - countParams = [req.user.system_id]; - break; - case ROLES.ADMIN: - getActionLogQuery = `SELECT action_log.action_log_id, action_log.submission_datetime AS submission_datetime, action_log.action_template, action_log.system_id, action_log.mock_id, action_log.project, - actions.action_target, actions.action_title, actions.semester, - projects.display_name, projects.title, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) mock_name - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - JOIN projects ON projects.project_id = action_log.project - ORDER BY submission_datetime DESC - LIMIT ? OFFSET ?`; - queryParams = [resultLimit, offset * resultLimit]; - getActionLogCount = `SELECT COUNT(*) FROM action_log`; - break; - default: - const error = new Error("Unknown Role"); - error.statusCode = 401; - return next(error); - } - - const actionLogsPromise = db.query(getActionLogQuery, queryParams); - const actionLogsCountPromise = db.query(getActionLogCount, countParams); - Promise.all([actionLogsCountPromise, actionLogsPromise]) - .then(([[actionLogCount], projects]) => { - res.send({ - actionLogCount: actionLogCount[Object.keys(actionLogCount)[0]], - actionLogs: projects, - }); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getTimeLogs", - [UserAuth.isSignedIn], - async (req, res, next) => { - let getTimeLogQuery = ""; - let params = []; - - switch (req.user.type) { - case ROLES.STUDENT: - // NOTE: Technically, users are able to see if coaches submitted time logs to other projects, but they should not be able to see the actual submission content form this query so that should be fine - // This is because of the "OR users.type = '${ROLES.COACH}'" part of the following query. - getTimeLogQuery = `SELECT time_log.time_log_id, time_log.submission_datetime, time_log.time_amount, time_log.system_id, time_log.mock_id, time_log.project, time_log.work_date, time_log.work_comment,time_log.active, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) mock_name - FROM time_log - WHERE time_log.project = ? - ORDER BY - time_log.work_date DESC`; - params = [req.user.project]; - break; - case ROLES.COACH: - case ROLES.ADMIN: - getTimeLogQuery = `SELECT time_log.*, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) AS name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) AS mock_name - FROM time_log - WHERE time_log.project = ?`; - params = [req.query.project_id]; - break; - - default: - const error = new Error("Unknown Role"); - error.statusCode = 401; - return next(error); - } - db.query(getTimeLogQuery, params) - .then((values) => { - res.send(values); - }) - .catch((err) => { - console.error(err); - error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getAllTimeLogs", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { resultLimit, offset } = req.query; - - let getTimeLogQuery = ""; - let queryParams = []; - let getTimeLogCount = ""; - let countParams = []; - - switch (req.user.type) { - case ROLES.STUDENT: - getTimeLogQuery = `SELECT time_log.*, - projects.display_name, projects.title, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) mock_name - FROM time_log - JOIN projects ON projects.project_id = time_log.project - WHERE time_log.project = ? - ORDER BY - time_log.work_date DESC`; - queryParams = [req.user.project]; - getTimeLogCount = `SELECT COUNT(*) FROM time_log - WHERE time_log.project = ? - AND time_log.system_id in (SELECT users.system_id FROM users WHERE users.project = ?)`; - countParams = [req.user.project, req.user.project]; - break; - case ROLES.COACH: - getTimeLogQuery = `SELECT time_log.*, - projects.display_name, projects.title, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) mock_name - FROM time_log - JOIN projects ON projects.project_id = time_log.project - WHERE time_log.project IN (SELECT project_id FROM project_coaches WHERE coach_id = ?) - ORDER BY time_log.work_date DESC`; - queryParams = [req.user.system_id]; - getTimeLogCount = `SELECT COUNT(*) FROM time_log WHERE time_log.project IN (SELECT project_id FROM project_coaches WHERE coach_id = ?)`; - countParams = [req.user.system_id]; - break; - case ROLES.ADMIN: - getTimeLogQuery = `SELECT time_log.*, - projects.display_name, projects.title, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) name, - (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) mock_name - FROM time_log - JOIN projects ON projects.project_id = time_log.project - ORDER BY time_log.work_date DESC`; - queryParams = []; - getTimeLogCount = `SELECT COUNT(*) FROM time_log`; - break; - default: - const error = new Error("Unknown Role"); - error.statusCode = 401; - return next(error); - } - - const timeLogsPromise = db.query(getTimeLogQuery, queryParams); - const timeLogsCountPromise = db.query(getTimeLogCount, countParams); - Promise.all([timeLogsCountPromise, timeLogsPromise]) - .then(([[timeLogCount], projects]) => { - res.send({ - timeLogCount: timeLogCount[Object.keys(timeLogCount)[0]], - timeLogs: projects, - }); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getCoachFeedback", - [UserAuth.isSignedIn], - async (req, res, next) => { - const getFeedbackQuery = ` - SELECT form_data, a.action_title as title, a.start_date as date, a.action_id, submission_datetime - FROM action_log - JOIN main.users u on action_log.system_id = u.system_id - JOIN main.actions a on action_log.action_template = a.action_id - WHERE action_log.project = ? AND u.type = 'coach' - `; - - db.query(getFeedbackQuery, req.query.project_id) - .then((feedback) => { - res.send(feedback); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/getAllSponsors", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { resultLimit, offset } = req.query; - - let getSponsorsQuery = ""; - let queryParams = []; - let getSponsorsCount = ""; - - switch (req.user.type) { - case ROLES.STUDENT: - break; - case ROLES.COACH: - case ROLES.ADMIN: - getSponsorsQuery = ` - SELECT * - FROM sponsors - ORDER BY - sponsors.company ASC, - sponsors.division ASC, - sponsors.fname ASC, - sponsors.lname ASC - LIMIT ? - OFFSET ? - `; - queryParams = [resultLimit || -1, offset || 0]; - getSponsorsCount = `SELECT COUNT(*) FROM sponsors`; - break; - default: - const error = new Error("Unknown Role"); - error.statusCode = 401; - return next(error); - } - - const sponsorsPromise = db.query(getSponsorsQuery, queryParams); - const SponsorsCountPromise = db.query(getSponsorsCount); - Promise.all([SponsorsCountPromise, sponsorsPromise]) - .then(([[sponsorsCount], sponsorsRows]) => { - res.send({ - sponsorsCount: sponsorsCount[Object.keys(sponsorsCount)[0]], - sponsors: sponsorsRows, - }); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get("/getProjectSponsor", [UserAuth.isSignedIn], (req, res) => { - let query = `SELECT * FROM sponsors - WHERE sponsor_id = (SELECT sponsor FROM projects WHERE project_id = ?)`; - - params = [req.query.project_id]; - db.query(query, params).then((users) => res.send(users)); - }); - - /** - * This is for getting the archive data to view/edit based on a specific ID. - * */ - db_router.get("/getArchiveProject", [UserAuth.isAdmin], (req, res) => { - let query = ` - SELECT * - FROM archive - WHERE archive_id = ? - `; - - const params = [req.query.archive_id]; - db.query(query, params).then((project) => res.send(project)); - }); - - db_router.get( - "/getSponsorProjects", - [UserAuth.isCoachOrAdmin], - (req, res) => { - let query = ` - SELECT * - FROM projects - WHERE sponsor = ? - `; - - const params = [req.query.sponsor_id]; - db.query(query, params).then((projects) => res.send(projects)); - }, - ); - - db_router.get( - "/getSponsorNotes", - [UserAuth.isCoachOrAdmin], - (req, res, next) => { - let getSponsorNotesQuery = ` - SELECT sponsor_notes.*, - users.fname, users.lname, users.email, users.type, - (SELECT users.fname || ' ' || users.lname FROM users WHERE users.system_id = sponsor_notes.mock_id) AS mock_name - FROM sponsor_notes - JOIN users - ON users.system_id = sponsor_notes.author - WHERE sponsor_notes.sponsor = ? - ORDER BY creation_date - `; - - const queryParams = [req.query.sponsor_id]; - - db.query(getSponsorNotesQuery, queryParams) - .then((sponsorNotes) => { - res.send(sponsorNotes); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get("/getSubmission", [UserAuth.isSignedIn], (req, res, next) => { - let getSubmissionQuery = ""; - let params = []; - - switch (req.user.type) { - case ROLES.STUDENT: - getSubmissionQuery = `SELECT action_log.form_data, action_log.files - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - WHERE action_log.action_log_id = ? AND (actions.action_target = '${ACTION_TARGETS.TEAM}' OR action_log.system_id = ?)`; - params = [req.query.log_id, req.user.system_id]; - break; - case ROLES.COACH: - getSubmissionQuery = `SELECT action_log.form_data, action_log.files - FROM action_log - JOIN project_coaches ON project_coaches.project_id = action_log.project - WHERE action_log.action_log_id = ? AND project_coaches.coach_id = ?`; - params = [req.query.log_id, req.user.system_id]; - break; - case ROLES.ADMIN: - getSubmissionQuery = `SELECT action_log.form_data, action_log.files - FROM action_log - WHERE action_log.action_log_id = ?`; - params = [req.query.log_id]; - break; - default: - const error = new Error("Unknown Role"); - error.statusCode = 401; - return next(error); - } - - db.query(getSubmissionQuery, params) - .then((submissions) => { - res.send(submissions); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.get( - "/getSubmissionFile", - [UserAuth.isSignedIn], - async (req, res, next) => { - let getSubmissionQuery = ""; - let params = []; - - switch (req.user.type) { - case ROLES.STUDENT: - getSubmissionQuery = `SELECT action_log.files, action_log.project, action_log.system_id, actions.action_id, actions.action_target - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - WHERE action_log.action_log_id = ? AND (actions.action_target = '${ACTION_TARGETS.TEAM}' OR action_log.system_id = ?)`; - params = [req.query.log_id, req.user.system_id]; - break; - case ROLES.COACH: - getSubmissionQuery = `SELECT action_log.files, action_log.project, action_log.system_id, actions.action_id, actions.action_target - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - JOIN project_coaches ON project_coaches.project_id = action_log.project - WHERE action_log.action_log_id = ? AND project_coaches.coach_id = ?`; - params = [req.query.log_id, req.user.system_id]; - break; - case ROLES.ADMIN: - getSubmissionQuery = `SELECT action_log.files, action_log.project, action_log.system_id, actions.action_id, actions.action_target - FROM action_log - JOIN actions ON actions.action_id = action_log.action_template - WHERE action_log.action_log_id = ?`; - params = [req.query.log_id]; - break; - default: - const error = new Error("Unknown Role"); - error.statusCode = 401; - return next(error); - } - - const { files, project, action_target, system_id, action_id } = - (await db.query(getSubmissionQuery, params))[0] || {}; - - let fileList = []; - if (files) { - fileList = files.split(","); - } - - if ( - fileList.includes(req.query.file) && - project && - action_target && - system_id && - action_id - ) { - return res.sendFile( - path.join( - __dirname, - `../project_docs/${project}/${action_target}/${action_id}/${system_id}/${req.query.file}`, - ), - ); - } - const error = new Error( - "File not found or you are unauthorized to view file", - ); - error.statusCode = 404; - return next(error); - }, - ); - - db_router.post( - "/editAction", - [UserAuth.isAdmin, UserAuth.canWrite, body("page_html").unescape()], - (req, res, next) => { - let body = req.body; - - let updateQuery = ` - UPDATE actions - SET semester = ?, - action_title = ?, - action_target = ?, - date_deleted = ?, - short_desc = ?, - start_date = ?, - due_date = ?, - page_html = ?, - file_types = ?, - file_size = ? - WHERE action_id = ? - `; - - const date_deleted = - body.date_deleted === "false" - ? moment().format(CONSTANTS.datetime_format) - : ""; - const parsedFileSize = body.file_size - ? fileSizeParser(body.file_size) - : null; - - let params = [ - body.semester, - body.action_title, - body.action_target, - date_deleted, - body.short_desc, - body.start_date, - body.due_date, - body.page_html, - body.file_types, - parsedFileSize, - body.action_id, - ]; - - db.query(updateQuery, params) - .then(() => { - return res.status(200).send(); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.get( - "/searchForSponsor", - [UserAuth.isCoachOrAdmin, body("page_html").escape()], - (req, res, next) => { - const { resultLimit, offset, searchQuery } = req.query; - - let getSponsorsQuery = ""; - let queryParams = []; - let getSponsorsCount = ""; - let sponsorCountParams = []; - - switch (req.user.type) { - case ROLES.STUDENT: - break; - case ROLES.COACH: - case ROLES.ADMIN: - getSponsorsQuery = ` - SELECT * - FROM sponsors - WHERE sponsors.OID NOT IN ( - SELECT OID - FROM sponsors - WHERE - company LIKE ? - OR division LIKE ? - OR fname LIKE ? - OR lname LIKE ? - ORDER BY - company, - division, - fname, - lname - LIMIT ? - ) AND ( - sponsors.company LIKE ? - OR sponsors.division LIKE ? - OR sponsors.fname LIKE ? - OR sponsors.lname LIKE ? - ) - ORDER BY - sponsors.company, - sponsors.division, - sponsors.fname, - sponsors.lname - LIMIT ? - `; - getSponsorsCount = `SELECT COUNT(*) - FROM sponsors - WHERE - company LIKE ? - OR division LIKE ? - OR fname LIKE ? - OR lname LIKE ? - `; - const searchQueryParam = searchQuery || ""; - queryParams = [ - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - offset || 0, - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - resultLimit || 0, - ]; - sponsorCountParams = [ - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - ]; - - break; - default: - const error = new Error("Unknown Role"); - error.statusCode = 401; - return next(error); - } - - const sponsorsPromise = db.query(getSponsorsQuery, queryParams); - const SponsorsCountPromise = db.query( - getSponsorsCount, - sponsorCountParams, - ); - Promise.all([SponsorsCountPromise, sponsorsPromise]) - .then(([[sponsorsCount], sponsorsRows]) => { - res.send({ - sponsorsCount: sponsorsCount[Object.keys(sponsorsCount)[0]], - sponsors: sponsorsRows, - }); - }) - .catch((error) => { - res.status(500).send(error); - }); - }, - ); - - db_router.get("/searchForArchive", (req, res, next) => { - const { resultLimit, offset, searchQuery, inactive } = req.query; - let skipNum = offset * resultLimit; - let getProjectsQuery = ""; - let queryParams = []; - let getProjectsCount = ""; - let projectCountParams = []; - - // allow inactive projects in search - if (inactive === "true") { - getProjectsQuery = `SELECT * FROM archive WHERE - archive.OID NOT IN ( - SELECT OID - FROM archive - WHERE title like ? - OR sponsor like ? - OR members like ? - OR coach like ? - OR keywords like ? - OR synopsis like ? - OR url_slug like ? - ORDER BY title, - sponsor, - members, - coach, - keywords, - synopsis, - url_slug - LIMIT ? - ) AND ( - archive.title like ? - OR archive.sponsor like ? - OR archive.members like ? - OR archive.coach like ? - OR archive.keywords like ? - OR archive.synopsis like ? - OR archive.url_slug like ? - ) - ORDER BY - archive.title, - archive.sponsor, - archive.members, - archive.coach, - archive.keywords, - archive.synopsis, - archive.url_slug - LIMIT ?`; - - getProjectsCount = `SELECT COUNT(*) - FROM archive - WHERE - title like ? - OR sponsor like ? - OR members like ? - OR coach like ? - OR keywords like ? - OR synopsis like ? - OR url_slug like ? - `; - } else { - getProjectsQuery = `SELECT * FROM archive WHERE - archive.OID NOT IN ( - SELECT OID - FROM archive - WHERE title like ? - OR sponsor like ? - OR members like ? - OR coach like ? - OR keywords like ? - OR synopsis like ? - OR url_slug like ? - AND inactive = '' - ORDER BY title, - sponsor, - members, - coach, - keywords, - synopsis, - url_slug - LIMIT ? - ) AND ( - archive.title like ? - OR archive.sponsor like ? - OR archive.members like ? - OR archive.coach like ? - OR archive.keywords like ? - OR archive.synopsis like ? - OR archive.url_slug like ? - AND inactive = '' - ) - ORDER BY - archive.title, - archive.sponsor, - archive.members, - archive.coach, - archive.keywords, - archive.synopsis, - archive.url_slug - LIMIT ?`; - - getProjectsCount = `SELECT COUNT(*) - FROM archive - WHERE - title like ? - OR sponsor like ? - OR members like ? - OR coach like ? - OR keywords like ? - OR synopsis like ? - OR url_slug like ? - AND (inactive = '' OR inactive IS NULL) - `; - } - - const searchQueryParam = searchQuery || ""; - - queryParams = [ - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - skipNum || 0, - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - resultLimit || 0, - ]; - projectCountParams = [ - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - "%" + searchQueryParam + "%", - ]; - - const projectPromise = db.query(getProjectsQuery, queryParams); - const projectCountPromise = db.query(getProjectsCount, projectCountParams); - Promise.all([projectCountPromise, projectPromise]) - .then(([[projectCount], projectRows]) => { - res.send({ - projectCount: projectCount[Object.keys(projectCount)[0]], - projects: projectRows, - }); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.get("/getArchiveFromSlug", (req, res, next) => { - let query = `SELECT * FROM archive WHERE url_slug=?`; - let params = [req.query.url_slug]; - db.query(query, params) - .then((values) => { - res.status(200).send(values); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.get("/getArchiveFromProject", (req, res) => { - let query = `SELECT * FROM archive WHERE archive.project_id=?`; - let params = [req.query.project_id]; - db.query(query, params) - .then((values) => { - res.status(200).send(values); - }) - .catch((err) => { - console.error(err); - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); - - db_router.post( - "/createSponsor", - [UserAuth.isCoachOrAdmin, UserAuth.canWrite, body("page_html").unescape()], - (req, res, next) => { - let body = req.body; - - let createSponsorQuery = ` - INSERT into sponsors( - fname, - lname, - company, - division, - email, - phone, - association, - type - ) - values (?,?,?,?,?,?,?,?) - `; - - let createSponsorParams = [ - body.fname, - body.lname, - body.company, - body.division, - body.email, - body.phone, - body.association, - body.type, - ]; - - let createSponsorQueryPromise = db - .query(createSponsorQuery, createSponsorParams) - .then(() => { - return [200, null]; - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - - let note_content = "Sponsor created by " + req.user.system_id; - - let createSponsorNoteParams = [ - note_content, - body.sponsor_id, - req.user.system_id, - null, - ]; - - let createSponsorNotePromise = createSponsorNote(createSponsorNoteParams); - - Promise.all([createSponsorQueryPromise, createSponsorNotePromise]).then( - ([ - [createSponsorQueryStatusCode, createSponsorError], - [createNoteStatusCode, createNoteError], - ]) => { - if (createSponsorError) { - res.status(createSponsorQueryStatusCode).send(createSponsorError); - } else if (createNoteError) { - res.status(createNoteStatusCode).send(createNoteError); - } else if (createSponsorQueryStatusCode !== createNoteStatusCode) { - const error = new Error( - "status code mismatch in editing sponsor, please contact an admin to investigate", - ); - error.statusCode = 500; - return next(error); - } else { - res.status(createSponsorQueryStatusCode).send(); - } - }, - ); - }, - ); - - db_router.post( - "/editSponsor", - [UserAuth.isCoachOrAdmin, UserAuth.canWrite, body("page_html").unescape()], - (req, res, next) => { - let body = req.body; - - let updateSponsorQuery = ` - UPDATE sponsors - SET fname = ?, - lname = ?, - company = ?, - division = ?, - email = ?, - phone = ?, - association = ?, - type = ?, - inActive = ?, - doNotEmail = ? - WHERE sponsor_id = ? - `; - - /** - * This is done so that the sponsors table boolean (int) columns can be updated correctly, without them working - * against the existing code inside of DatabaseTableEditor.js - **/ - let inActive = body.inActive === "true" || body.inActive === "1"; - let doNotEmail = body.doNotEmail === "true" || body.doNotEmail === "1"; - - let updateSponsorParams = [ - body.fname, - body.lname, - body.company, - body.division, - body.email, - body.phone, - body.association, - body.type, - inActive, - doNotEmail, - body.sponsor_id, - ]; - - let updateQueryPromise = db - .query(updateSponsorQuery, updateSponsorParams) - .then(() => { - return [200, null]; - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - - let changedFieldsMessageFirstPart = []; - let changedFieldsMessageSecondPart = []; - let changedFieldsMessageThirdPart = []; - - body.changed_fields = JSON.parse(body.changed_fields); - - for (const field of Object.keys(body.changed_fields)) { - changedFieldsMessageFirstPart.push(field); - changedFieldsMessageSecondPart.push(body.changed_fields[field][0]); - changedFieldsMessageThirdPart.push(body.changed_fields[field][1]); - } - - let note_content = - "Fields: " + - changedFieldsMessageFirstPart.join(", ") + - " were changed from: " + - changedFieldsMessageSecondPart.join(", ") + - " to: " + - changedFieldsMessageThirdPart.join(", "); - - let createSponsorNoteParams = [ - note_content, - body.sponsor_id, - req.user.system_id, - null, - ]; - - let createSponsorNotePromise = createSponsorNote(createSponsorNoteParams); - - Promise.all([updateQueryPromise, createSponsorNotePromise]).then( - ([ - [updateQueryStatusCode, updateSponsorError], - [createNoteStatusCode, createNoteError], - ]) => { - if (updateSponsorError) { - res.status(updateQueryStatusCode).send(updateSponsorError); - } else if (createNoteError) { - res.status(createNoteStatusCode).send(createNoteError); - } else if (updateQueryStatusCode !== createNoteStatusCode) { - const error = new Error( - "status code mismatch in editing sponsor, please contact an admin to investigate", - ); - error.statusCode = 500; - return next(error); - } else { - res.status(updateQueryStatusCode).send(); - } - }, - ); - }, - ); - - async function createSponsorNote(queryParams) { - let insertQuery = ` - INSERT into sponsor_notes - (note_content, sponsor, author, mock_id, previous_note) - values (?, ?, ?, ?, ?)`; - - let status = 500; - let error = null; - - await db - .query(insertQuery, queryParams) - .then(() => { - status = 200; - }) - .catch((err) => { - status = 500; - error = err; - }); - return [status, error]; - } - - db_router.post( - "/createSponsorNote", - [UserAuth.isCoachOrAdmin, UserAuth.canWrite, body("page_html").unescape()], - (req, res, next) => { - let body = req.body; - let mock_id = req.user.mock ? req.user.mock.system_id : null; - - params = [ - body.note_content, - body.sponsor_id, - req.user.system_id, - mock_id, - body.previous_note, - ]; - - createSponsorNote(params).then(([status, err]) => { - if (err) { - const error = new Error(err); - error.statusCode = status; - return next(error); - } else { - res.status(status).send(); - } - }); - }, - ); - - db_router.post( - "/createAction", - [UserAuth.isAdmin, UserAuth.canWrite, body("page_html").unescape()], + db_router.get( + "/getActiveTimelines", + [UserAuth.isSignedIn], (req, res, next) => { - let body = req.body; - - let updateQuery = ` - INSERT into actions - (semester, action_title, action_target, date_deleted, short_desc, start_date, due_date, page_html, file_types, file_size) - values (?,?,?,?,?,?,?,?,?,?)`; - - const date_deleted = - body.date_deleted === "false" - ? moment().format(CONSTANTS.datetime_format) - : ""; - const parsedFileSize = body.file_size - ? fileSizeParser(body.file_size) - : null; - - let params = [ - body.semester, - body.action_title, - body.action_target, - date_deleted, - body.short_desc, - body.start_date, - body.due_date, - body.page_html, - body.file_types, - parsedFileSize, - ]; - - db.query(updateQuery, params) - .then(() => { - return res.status(200).send(); + calculateActiveTimelines(req.user) + .then((timelines) => { + res.json(timelines); }) .catch((err) => { + console.error(err); const error = new Error(err); error.statusCode = 500; return next(error); @@ -3886,181 +102,78 @@ module.exports = (db) => { }, ); - db_router.get("/getSemesters", [UserAuth.isSignedIn], (req, res, next) => { - let getSemestersQuery = ` - SELECT * - FROM semester_group - ORDER BY end_date, start_date, name - `; - db.query(getSemestersQuery) - .then((values) => { - res.send(values); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }); + db_router.get("/getHtml", (req, res, next) => { + let getHtmlQuery = `SELECT * FROM page_html`; + let queryParams = []; + //If there is a query parameter, then select html from specified table. + if (typeof req.query.name !== "undefined" && req.query.name) { + getHtmlQuery = `SELECT html FROM page_html WHERE name = ?`; + queryParams = [req.query.name]; + } + db.query(getHtmlQuery, queryParams) + .then((html) => { + // Replace placeholder with actual server base URL + const serverBaseUrl = + process.env.NODE_ENV === "production" + ? process.env.PRODUCTION_SERVER_URL || + process.env.BASE_URL || + `${req.protocol}://${req.get("host")}` + : `${req.protocol}://${req.get("host")}`; + + // Process HTML to replace placeholders + if (Array.isArray(html)) { + html = html.map((item) => { + if (item.html) { + item.html = item.html.replace( + /__SERVER_BASE_URL__/g, + serverBaseUrl, + ); + } + return item; + }); + } else if (html && html.html) { + html.html = html.html.replace(/__SERVER_BASE_URL__/g, serverBaseUrl); + } - db_router.get("/getArchive", [UserAuth.isAdmin], (req, res, next) => { - let getArchiveQuery = ` - SELECT * - FROM archive`; - db.query(getArchiveQuery) - .then((values) => { - res.send(values); + res.send(html); }) .catch((err) => { + console.error(err); const error = new Error(err); error.statusCode = 500; return next(error); }); }); - db_router.get( - "/getSemesterAnnouncements", - [UserAuth.isSignedIn], - (req, res, next) => { - let filter = ""; - if (req.user.type === ROLES.STUDENT) { - // req.query.semester comes in as a string and req.user.semester_group is a number so convert both to strings to compare them. - if (`${req.query.semester}` !== `${req.user.semester_group}`) { - const error = new Error( - "Students can not access announcements that are not for your project", - ); - error.statusCode = 401; - return next(error); - } - - filter = `AND actions.action_target IS NOT '${ACTION_TARGETS.COACH_ANNOUNCEMENT}'`; - // Note: Since we only do this check for students, coaches can technically hack the request to see announcements for other semesters. - // Unfortunately, coaches don't inherently have a semester like students do - // and 1am Kevin can't think of another way of ensuring that a coach isn't lying to us about their semester ...but idk what they would gain form doing that sooo ima just leave it for now - } - - //ToDo: make sure that the dates don't screw things up because of GMT i.e. it becomes tomorrow in GMT before it becomes tomorrow at the server's location - let getTimelineActions = ` - SELECT action_title, action_id, start_date, due_date, semester, action_target, date_deleted, page_html - FROM actions - WHERE actions.date_deleted = '' AND actions.semester = ? - AND (actions.action_target IN ('${ACTION_TARGETS.COACH_ANNOUNCEMENT}', '${ACTION_TARGETS.STUDENT_ANNOUNCEMENT}') AND actions.start_date <= date('now') AND actions.due_date >= date('now')) - ${filter} - ORDER BY actions.due_date ASC - `; - - db.query(getTimelineActions, [req.query.semester]) - .then((values) => { - res.send(values); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/editSemester", - [UserAuth.isAdmin, UserAuth.canWrite, body("*").trim()], - (req, res, next) => { - let body = req.body; - - let updateQuery = ` - UPDATE semester_group - SET name = ?, - dept = ?, - start_date = ?, - end_date = ? - WHERE semester_id = ? - `; - - let params = [ - body.name, - body.dept, - body.start_date, - body.end_date, - body.semester_id, - ]; - - db.query(updateQuery, params) - .then(() => { - return res.status(200).send(); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); - }, - ); - db_router.post( - "/createSemester", - [ - UserAuth.isAdmin, - UserAuth.canWrite, - body("name") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("dept") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("start_date") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - body("end_date") - .not() - .isEmpty() - .trim() - .escape() - .withMessage("Cannot be empty") - .isLength({ max: 50 }), - ], + "/editPage", + [UserAuth.isAdmin, UserAuth.canWrite], (req, res, next) => { - let result = validationResult(req); - - if (result.errors.length !== 0) { - const errorMessages = result.errors - .map((error) => `${error.param}: ${error.msg}`) - .join(", "); - const error = new Error(`Error Creating Semester: ${errorMessages}`); - error.statusCode = 400; - return next(error); - } - - let body = req.body; - - let sql = ` - INSERT INTO semester_group - (name, dept, start_date, end_date) - VALUES (?,?,?,?); + let editPageQuery = `UPDATE page_html + SET html = ? + WHERE name = ? `; - - let params = [body.name, body.dept, body.start_date, body.end_date]; - - db.query(sql, params) - .then(() => { - return res.status(200).send(); - }) - .catch((err) => { - const error = new Error(err); - error.statusCode = 500; - return next(error); - }); + let promises = []; + //Individually update all html tables from the body of the req. + Object.keys(req.body).forEach((key) => { + let queryParams = [req.body[key], key]; + promises.push( + db + .query(editPageQuery, queryParams) + .then(() => { + //do nothing + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }), + ); + }); + Promise.all(promises).then(() => { + res.send({ msg: "Success!" }); + }); }, ); @@ -4117,387 +230,5 @@ module.exports = (db) => { }); } - db_router.get( - "/getAdditionalInfo", - [UserAuth.isSignedIn], - async (req, res, next) => { - const requestedUserId = req.query.system_id; - - if (!requestedUserId) { - return res.status(400).send({ error: "User ID is required" }); - } - - try { - const result = await db.query( - `SELECT json_extract(profile_info, '$.additional_info') AS additional_info - FROM users WHERE system_id = ?`, - [requestedUserId], - ); - - if (result.length === 0) { - const error = new Error("User not found"); - error.statusCode = 404; - return next(error); - } - - res.send(result[0]); - } catch (err) { - const error = new Error("Database query failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - db_router.post( - "/editAdditionalInfo", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id, additional_info } = req.body; - - if (!system_id || additional_info === undefined) { - return res - .status(400) - .send({ error: "system_id and additional_info are required" }); - } - - const updateQuery = ` - UPDATE users - SET profile_info = json_set(profile_info, '$.additional_info', ?) - WHERE system_id = ? - `; - - try { - await db.query(updateQuery, [additional_info, system_id]); - res - .status(200) - .send({ message: "Additional info updated successfully" }); - } catch (err) { - const error = new Error("Database update failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - db_router.post( - "/setDarkMode", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id, dark_mode } = req.body; - - const updateQuery = ` - UPDATE users - SET profile_info = json_set(profile_info, '$.dark_mode', ?) - WHERE system_id = ? - `; - - try { - await db.query(updateQuery, [dark_mode, system_id]); - res - .status(200) - .send({ message: "Dark mode preference updated successfully" }); - } catch (err) { - const error = new Error("Database update failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - db_router.get( - "/getDarkMode", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id } = req.query; - - const query = ` - SELECT JSON_EXTRACT(profile_info, '$.dark_mode') AS dark_mode - FROM users - WHERE system_id = ? - `; - - try { - const result = await db.query(query, [system_id]); - if (result.length === 0) { - const error = new Error("User not found"); - error.statusCode = 404; - return next(error); - } - - const darkModeRaw = result[0].dark_mode; - const dark_mode = Boolean(darkModeRaw); - - res.status(200).send({ dark_mode }); - } catch (err) { - const error = new Error("Database query failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - db_router.get( - "/getPeerEvals", - [UserAuth.isCoachOrAdmin], - (req, res, next) => { - const semesterNumber = req.query.semester; - - let getPeerEvalsQuery = ` - SELECT action_id - FROM actions - WHERE action_target = 'peer_evaluation' - `; - - let queryParams = []; - if (semesterNumber) { - getPeerEvalsQuery += ` AND semester = ?`; - queryParams.push(semesterNumber); - } - - db.query(getPeerEvalsQuery, queryParams) - .then((values) => { - const actionIds = values.map((row) => row.action_id); - - if (actionIds.length === 0) { - return res.send([]); - } - - let getPeerEvalLogsQuery = ` - SELECT action_log.*, users.fname, users.lname, users.type - FROM action_log - LEFT JOIN users ON action_log.system_id = users.system_id - WHERE action_template IN (${actionIds.join(",")}) - ORDER BY submission_datetime DESC - `; - - return db.query(getPeerEvalLogsQuery); - }) - .then((logs) => { - if (logs) res.send(logs); - }) - .catch((err) => { - console.error(err); - const error = new Error("Error fetching peer evaluations"); - error.statusCode = 500; - return next(error); - }); - }, - ); - - db_router.post( - "/setGanttView", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id, gantt_view } = req.body; - - const updateQuery = ` - UPDATE users - SET profile_info = json_set(profile_info, '$.gantt_view', ?) - WHERE system_id = ? - `; - - try { - await db.query(updateQuery, [gantt_view, system_id]); - console.log( - "Gantt view preference updated successfully", - gantt_view, - system_id, - ); - res - .status(200) - .send({ message: "Gantt view preference updated successfully" }); - } catch (err) { - const error = new Error("Database update failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - db_router.get( - "/getGanttView", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id } = req.query; - - const query = ` - SELECT JSON_EXTRACT(profile_info, '$.gantt_view') AS gantt_view - FROM users - WHERE system_id = ? - `; - - try { - const result = await db.query(query, [system_id]); - - if (result.length === 0) { - const error = new Error("User not found"); - error.statusCode = 404; - return next(error); - } - - const ganttViewRaw = result[0].gantt_view; - // Handle both boolean and string values from JSON - const gantt_view = - ganttViewRaw === true || - ganttViewRaw === "true" || - ganttViewRaw === 1 || - ganttViewRaw === "1"; - - res.status(200).send({ gantt_view }); - } catch (err) { - const error = new Error("Database query failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - // Calendar View preferences - db_router.post( - "/setCalendarView", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id, calendar_view } = req.body; - - const updateQuery = ` - UPDATE users - SET profile_info = json_set(profile_info, '$.calendar_view', ?) - WHERE system_id = ? - `; - - try { - await db.query(updateQuery, [calendar_view, system_id]); - console.log( - "Calendar view preference updated successfully", - calendar_view, - system_id, - ); - res - .status(200) - .send({ message: "Calendar view preference updated successfully" }); - } catch (err) { - const error = new Error("Database update failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - db_router.get( - "/getCalendarView", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id } = req.query; - - const query = ` - SELECT JSON_EXTRACT(profile_info, '$.calendar_view') AS calendar_view - FROM users - WHERE system_id = ? - `; - - try { - const result = await db.query(query, [system_id]); - - if (result.length === 0) { - const error = new Error("User not found"); - error.statusCode = 404; - return next(error); - } - const calendarViewRaw = result[0].calendar_view; - // Handle both boolean and string values from JSON - const calendar_view = - calendarViewRaw === true || - calendarViewRaw === "true" || - calendarViewRaw === 1 || - calendarViewRaw === "1"; - - res.status(200).send({ calendar_view }); - } catch (err) { - const error = new Error("Database query failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - // Milestone View preferences - db_router.post( - "/setMilestoneView", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id, milestone_view } = req.body; - - const updateQuery = ` - UPDATE users - SET profile_info = json_set(profile_info, '$.milestone_view', ?) - WHERE system_id = ? - `; - - try { - await db.query(updateQuery, [milestone_view, system_id]); - console.log( - "Milestone view preference updated successfully", - milestone_view, - system_id, - ); - res - .status(200) - .send({ message: "Milestone view preference updated successfully" }); - } catch (err) { - const error = new Error("Database update failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - - db_router.get( - "/getMilestoneView", - [UserAuth.isSignedIn], - async (req, res, next) => { - const { system_id } = req.query; - - const query = ` - SELECT JSON_EXTRACT(profile_info, '$.milestone_view') AS milestone_view - FROM users - WHERE system_id = ? - `; - - try { - const result = await db.query(query, [system_id]); - - if (result.length === 0) { - const error = new Error("User not found"); - error.statusCode = 404; - return next(error); - } - const milestoneViewRaw = result[0].milestone_view; - // Handle both boolean and string values from JSON - const milestone_view = - milestoneViewRaw === true || - milestoneViewRaw === "true" || - milestoneViewRaw === 1 || - milestoneViewRaw === "1"; - - res.status(200).send({ milestone_view }); - } catch (err) { - const error = new Error("Database query failed"); - error.statusCode = 500; - error.details = err.message; - next(error); - } - }, - ); - return db_router; }; diff --git a/server/server/routing/functions/actions-func.js b/server/server/routing/functions/actions-func.js new file mode 100644 index 00000000..4dd813ab --- /dev/null +++ b/server/server/routing/functions/actions-func.js @@ -0,0 +1,535 @@ +const moment = require("moment"); +const fileSizeParser = require("filesize-parser"); +const path = require("path"); +const fs = require("fs"); +const { nanoid } = require("nanoid"); +const { ROLES } = require("../consts"); +const ACTION_TARGETS = { + ADMIN: "admin", + COACH: "coach", + INDIVIDUAL: "individual", + TEAM: "team", + PEER_EVALUATION: "peer_evaluation", + COACH_ANNOUNCEMENT: "coach_announcement", + STUDENT_ANNOUNCEMENT: "student_announcement", +}; + +const defaultFileSizeLimit = 15 * 1024 * 1024; + +function humanFileSize(bytes, si = false, dp = 1) { + const thresh = si ? 1000 : 1024; + + if (Math.abs(bytes) < thresh) { + return bytes + " B"; + } + + const units = ["KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + let u = -1; + const r = 10 ** dp; + + do { + bytes /= thresh; + ++u; + } while ( + Math.round(Math.abs(bytes) * r) / r >= thresh && + u < units.length - 1 + ); + + return bytes.toFixed(dp) + " " + units[u]; +} + +/** + * Get all actions + */ +const getActions = (db) => { + return new Promise((resolve, reject) => { + const query = `SELECT * FROM actions ORDER BY action_id desc`; + db.query(query) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get timeline actions for a project + */ +const getTimelineActions = (db, projectId, user) => { + return new Promise((resolve, reject) => { + const query = `SELECT action_title, action_id, start_date, due_date, semester, action_target, date_deleted, short_desc, file_types, file_size, page_html, + CASE + WHEN action_target IS 'admin' AND system_id IS NOT NULL THEN 'green' + WHEN action_target IS 'coach' AND system_id IS NOT NULL THEN 'green' + WHEN action_target IS 'team' AND system_id IS NOT NULL THEN 'green' + WHEN action_target = 'peer_evaluation' AND COUNT(DISTINCT system_id) IS (SELECT COUNT(DISTINCT system_id) FROM users WHERE users.project = ?) + 1 THEN 'green' + WHEN action_target = 'peer_evaluation' THEN 'red' + WHEN action_target IS 'individual' AND COUNT(DISTINCT system_id) IS (SELECT COUNT(DISTINCT system_id) FROM users WHERE users.project = ?) THEN 'green' + WHEN start_date <= date('now') AND due_date >= date('now') THEN 'yellow' + WHEN date('now') > due_date AND system_id IS NULL THEN 'red' + WHEN date('now') > due_date AND action_target IS 'individual' AND COUNT(DISTINCT system_id) != (SELECT COUNT(DISTINCT system_id) FROM users WHERE users.project = ?) THEN 'red' + WHEN date('now') < start_date THEN 'grey' + ELSE 'UNHANDLED-CASE' + END AS 'state' + FROM actions + LEFT JOIN action_log + ON action_log.action_template = actions.action_id AND action_log.project = ? + WHERE actions.date_deleted = '' AND actions.semester = (SELECT distinct projects.semester FROM projects WHERE projects.project_id = ?) + AND actions.action_target NOT IN ('${ACTION_TARGETS.COACH_ANNOUNCEMENT}', '${ACTION_TARGETS.STUDENT_ANNOUNCEMENT}') + GROUP BY actions.action_id`; + + if (user.type === ROLES.STUDENT && projectId !== user.project) { + reject(new Error("trying to acces project that is not yours")); + return; + } + + db.query(query, [projectId, projectId, projectId, projectId, projectId]) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get late submission info for an action log + */ +const getLateSubmission = (db, logId) => { + return new Promise((resolve, reject) => { + const query = `SELECT actions.due_date + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + WHERE action_log.action_log_id = ?`; + + db.query(query, [logId]) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get action logs for a specific action + */ +const getActionLogs = (db, user, actionId, projectId) => { + return new Promise((resolve, reject) => { + let query = ""; + let params = []; + + switch (user.type) { + case ROLES.STUDENT: + query = `SELECT action_log.action_log_id, action_log.submission_datetime, action_log.action_template, action_log.system_id, action_log.mock_id, action_log.project, + actions.action_title, actions.due_date, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) mock_name, + (SELECT users.type FROM users WHERE users.system_id = action_log.system_id) AS user_type, + (SELECT users.type FROM users WHERE users.system_id = action_log.mock_id) AS mock_type + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + WHERE action_log.action_template = ? AND action_log.project = ?`; + params = [actionId, user.project]; + break; + case ROLES.COACH: + case ROLES.ADMIN: + query = `SELECT action_log.*, actions.action_title, actions.due_date, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) AS name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) AS mock_name, + (SELECT users.type FROM users WHERE users.system_id = action_log.system_id) AS user_type, + (SELECT users.type FROM users WHERE users.system_id = action_log.mock_id) AS mock_type + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + WHERE action_log.action_template = ? AND action_log.project = ?`; + params = [actionId, projectId]; + break; + + default: + reject(new Error("Unknown Role")); + return; + } + + db.query(query, params) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all action logs with pagination + */ +const getAllActionLogs = (db, user, resultLimit, offset) => { + return new Promise((resolve, reject) => { + let query = ""; + let queryParams = []; + let countQuery = ""; + let countParams = []; + + switch (user.type) { + case ROLES.STUDENT: + query = `SELECT action_log.action_log_id, action_log.submission_datetime AS submission_datetime, action_log.action_template, action_log.system_id, action_log.mock_id, action_log.project, + actions.action_target, actions.action_title, actions.semester, + projects.display_name, projects.title, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) mock_name + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + JOIN projects ON projects.project_id = action_log.project + WHERE action_log.project = ? + ORDER BY submission_datetime DESC + LIMIT ? OFFSET ?`; + queryParams = [user.project, resultLimit, offset * resultLimit]; + countQuery = `SELECT COUNT(*) FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + WHERE action_log.project = ? + AND action_log.system_id in (SELECT users.system_id FROM users WHERE users.project = ?)`; + countParams = [user.project, user.project]; + break; + case ROLES.COACH: + query = `SELECT action_log.action_log_id, action_log.submission_datetime AS submission_datetime, action_log.action_template, action_log.system_id, action_log.mock_id, action_log.project, + actions.action_target, actions.action_title, actions.semester, + projects.display_name, projects.title, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) mock_name + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + JOIN projects ON projects.project_id = action_log.project + WHERE action_log.project IN (SELECT project_id FROM project_coaches WHERE coach_id = ?) + ORDER BY submission_datetime DESC + LIMIT ? OFFSET ?`; + queryParams = [user.system_id, resultLimit, offset * resultLimit]; + countQuery = `SELECT COUNT(*) FROM action_log WHERE action_log.project IN (SELECT project_id FROM project_coaches WHERE coach_id = ?)`; + countParams = [user.system_id]; + break; + case ROLES.ADMIN: + query = `SELECT action_log.action_log_id, action_log.submission_datetime AS submission_datetime, action_log.action_template, action_log.system_id, action_log.mock_id, action_log.project, + actions.action_target, actions.action_title, actions.semester, + projects.display_name, projects.title, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.system_id) name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = action_log.mock_id) mock_name + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + JOIN projects ON projects.project_id = action_log.project + ORDER BY submission_datetime DESC + LIMIT ? OFFSET ?`; + queryParams = [resultLimit, offset * resultLimit]; + countQuery = `SELECT COUNT(*) FROM action_log`; + break; + default: + reject(new Error("Unknown Role")); + return; + } + + const actionLogsPromise = db.query(query, queryParams); + const actionLogsCountPromise = db.query(countQuery, countParams); + + Promise.all([actionLogsCountPromise, actionLogsPromise]) + .then(([[actionLogCount], actionLogs]) => { + resolve({ + actionLogCount: actionLogCount[Object.keys(actionLogCount)[0]], + actionLogs: actionLogs, + }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Submit an action with file attachments + */ +const submitAction = (db, user, body, files, __dirname) => { + return new Promise(async (resolve, reject) => { + try { + const query = `SELECT * FROM actions WHERE action_id = ?;`; + const [action] = await db.query(query, [body.action_template]); + + const startDate = new Date(action.start_date); + if (startDate > Date.now()) { + reject(new Error("Action start date is in the future")); + return; + } + + // Validate action target + switch (action.action_target) { + case ACTION_TARGETS.ADMIN: + if (user.type !== ROLES.ADMIN) { + reject(new Error("Only admins can submit admin actions")); + return; + } + break; + case ACTION_TARGETS.COACH: + if (user.type !== ROLES.COACH && user.type !== ROLES.ADMIN) { + reject(new Error("Only coaches can submit coach actions")); + return; + } + break; + case ACTION_TARGETS.INDIVIDUAL: + if (user.type !== ROLES.STUDENT) { + reject(new Error("Only students can submit individual actions")); + return; + } + break; + case ACTION_TARGETS.PEER_EVALUATION: + if (user.type !== ROLES.COACH && user.type !== ROLES.STUDENT) { + reject( + new Error( + "Only coaches and students can submit peer evaluations", + ), + ); + return; + } + break; + case ACTION_TARGETS.COACH_ANNOUNCEMENT: + case ACTION_TARGETS.STUDENT_ANNOUNCEMENT: + reject(new Error("You cannot submit an announcement")); + return; + case ACTION_TARGETS.TEAM: + // Anyone can submit team actions + break; + default: + reject(new Error("Invalid action target")); + return; + } + + let date = new Date(); + let timeString = `${date.getFullYear()}-${date.getUTCMonth()}-${date.getDate()}`; + const submission = `${timeString}_${nanoid()}`; + + let baseURL = path.join( + __dirname, + `../project_docs/${body.project}/${action.action_target}/${action.action_id}/${user.system_id}/${submission}`, + ); + + // Attachment Handling + let filenamesCSV = ""; + if (files && files.attachments) { + // If there is only one attachment, then it does not come as a list + if (files.attachments.length === undefined) { + files.attachments = [files.attachments]; + } + + if (files.attachments.length > 5) { + // Don't allow more than 5 files + reject(new Error("Maximum of 5 files allowed")); + return; + } + + fs.mkdirSync(baseURL, { recursive: true }); + + for (let x = 0; x < files.attachments.length; x++) { + if ( + files.attachments[x].size > + (action.file_size || defaultFileSizeLimit) + ) { + // 15mb limit exceeded + const responseText = + "File exceeded submission size limit of: " + + humanFileSize(action.file_size || defaultFileSizeLimit, false, 0); + reject(new Error(responseText)); + return; + } + if ( + !action.file_types + .split(",") + .includes( + path.extname(files.attachments[x].name).toLocaleLowerCase(), + ) + ) { + // send an error if the file is not an accepted type + reject(new Error("file type not accepted")); + return; + } + + // Append the file name to the CSV string, begin with a comma if x is not 0 + filenamesCSV += + x === 0 + ? `${submission}/${files.attachments[x].name}` + : `,${submission}/${files.attachments[x].name}`; + + files.attachments[x].mv( + `${baseURL}/${files.attachments[x].name}`, + function (err) { + if (err) { + reject(err); + } + }, + ); + } + } + + let insertAction = ` + INSERT INTO action_log( + action_template, + system_id, + project, + form_data, + files + ${(user.mock && ",mock_id") || ""} + ) + VALUES (?,?,?,?,?${(user.mock && ",?") || ""}) + `; + + let params = [ + body.action_template, + user.system_id, + body.project, + body.form_data, + filenamesCSV, + ]; + if (user.mock) { + params.push(user.mock.system_id); + } + + db.query(insertAction, params) + .then(() => { + resolve({ success: true }); + }) + .catch((err) => { + reject(err); + }); + } catch (err) { + reject(err); + } + }); +}; + +/** + * Get coach feedback for a project + */ +const getCoachFeedback = (db, projectId) => { + return new Promise((resolve, reject) => { + const query = ` + SELECT form_data, a.action_title as title, a.start_date as date, a.action_id, submission_datetime + FROM action_log + JOIN main.users u on action_log.system_id = u.system_id + JOIN main.actions a on action_log.action_template = a.action_id + WHERE action_log.project = ? AND u.type = 'coach' + `; + + db.query(query, [projectId]) + .then((feedback) => { + resolve(feedback); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Create a new action template + */ +const createAction = (db, body) => { + return new Promise((resolve, reject) => { + let updateQuery = ` + INSERT into actions + (semester, action_title, action_target, date_deleted, short_desc, start_date, due_date, page_html, file_types, file_size) + values (?,?,?,?,?,?,?,?,?,?)`; + + const date_deleted = + body.date_deleted === "false" + ? moment().format("YYYY-MM-DD HH:mm:ss") + : ""; + + const parsedFileSize = body.file_size + ? fileSizeParser(body.file_size) + : null; + + let params = [ + body.semester, + body.action_title, + body.action_target, + date_deleted, + body.short_desc, + body.start_date, + body.due_date, + body.page_html, + body.file_types, + parsedFileSize, + ]; + + db.query(updateQuery, params) + .then(() => { + resolve({ success: true }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Edit an existing action template + */ +const editAction = (db, body) => { + return new Promise((resolve, reject) => { + let updateQuery = ` + UPDATE actions + SET semester = ?, + action_title = ?, + action_target = ?, + date_deleted = ?, + short_desc = ?, + start_date = ?, + due_date = ?, + page_html = ?, + file_types = ?, + file_size = ? + WHERE action_id = ? + `; + + const date_deleted = + body.date_deleted === "false" + ? moment().format("YYYY-MM-DD HH:mm:ss") + : ""; + + const parsedFileSize = body.file_size + ? fileSizeParser(body.file_size) + : null; + + let params = [ + body.semester, + body.action_title, + body.action_target, + date_deleted, + body.short_desc, + body.start_date, + body.due_date, + body.page_html, + body.file_types, + parsedFileSize, + body.action_id, + ]; + + db.query(updateQuery, params) + .then(() => { + resolve({ success: true }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +module.exports = { + getActions, + getTimelineActions, + getLateSubmission, + getActionLogs, + getAllActionLogs, + submitAction, + getCoachFeedback, + createAction, + editAction, +}; diff --git a/server/server/routing/functions/archives-func.js b/server/server/routing/functions/archives-func.js new file mode 100644 index 00000000..8b0437c4 --- /dev/null +++ b/server/server/routing/functions/archives-func.js @@ -0,0 +1,708 @@ +const moment = require("moment"); +const DB_CONFIG = require("../database/db_config"); +const CONSTANTS = require("../consts"); +const path = require("path"); +const fs = require("fs"); + +/** + * Helper function to convert string to boolean + */ +const checkBox = (data) => { + if (data === "true" || data === "1") { + return 1; + } + return 0; +}; + +/** + * Helper function to convert string to integer + */ +const strToInt = (data) => { + if (typeof data === "string") { + return parseInt(data); + } + return 0; +}; + +/** + * Create a new archive entry + */ +const createArchive = (db, body, user) => { + return new Promise((resolve, reject) => { + const inactive = + body.inactive === "true" + ? moment().format(CONSTANTS.datetime_format) + : ""; + const locked = + body.locked === "true" + ? user.fname + + " " + + user.lname + + " locked at " + + moment().format(CONSTANTS.datetime_format) + : ""; + + const query = `INSERT INTO ${DB_CONFIG.tableNames.archive}(featured, outstanding, creative, + priority, title, project_id, team_name, members, sponsor, coach, poster_thumb, + poster_full, archive_image, synopsis, video, name, dept, start_date, end_date, + keywords, url_slug, inactive, locked) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + + const params = [ + checkBox(body.featured), + checkBox(body.outstanding), + checkBox(body.creative), + strToInt(body.priority), + body.title, + body.project_id, + body.team_name, + body.members, + body.sponsor, + body.coach, + body.poster_thumb, + body.poster_full, + body.archive_image, + body.synopsis, + body.video, + body.name, + body.dept, + body.start_date, + body.end_date, + body.keywords, + body.url_slug, + inactive, + locked, + ]; + + db.query(query, params) + .then((response) => { + resolve(response); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get active archive projects with pagination + */ +const getActiveArchiveProjects = (db, resultLimit, page, featured) => { + return new Promise((resolve, reject) => { + let skipNum = page * resultLimit; + let projectsQuery; + let rowCountQuery; + + if (featured === "true") { + projectsQuery = `SELECT * FROM ${DB_CONFIG.tableNames.archive} WHERE oid NOT IN + ( SELECT oid FROM ${DB_CONFIG.tableNames.archive} ORDER BY random() LIMIT ? ) + AND inactive = '' AND featured = 1 ORDER BY random() LIMIT ?`; + rowCountQuery = `SELECT COUNT(*) FROM ${DB_CONFIG.tableNames.archive} WHERE inactive = ''`; + } else { + projectsQuery = `SELECT * FROM ${DB_CONFIG.tableNames.archive} WHERE oid NOT IN + ( SELECT oid FROM ${DB_CONFIG.tableNames.archive} ORDER BY archive_id LIMIT ? ) + AND inactive = '' ORDER BY archive_id LIMIT ?`; + rowCountQuery = `SELECT COUNT(*) FROM ${DB_CONFIG.tableNames.archive} WHERE inactive = ''`; + } + + const projectsPromise = db.query(projectsQuery, [skipNum, resultLimit]); + const rowCountPromise = db.query(rowCountQuery); + + Promise.all([rowCountPromise, projectsPromise]) + .then(([[rowCount], projects]) => { + resolve({ + totalProjects: rowCount[Object.keys(rowCount)[0]], + projects: projects, + }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all archive projects with pagination (admin view) + */ +const getArchiveProjects = (db, resultLimit, offset) => { + return new Promise((resolve, reject) => { + let skipNum = offset * resultLimit; + let projectsQuery = `SELECT * FROM ${DB_CONFIG.tableNames.archive} WHERE + oid NOT IN (SELECT oid FROM ${DB_CONFIG.tableNames.archive} ORDER BY archive_id LIMIT ?) + ORDER BY archive_id LIMIT ?`; + let rowCountQuery = `SELECT COUNT(*) FROM ${DB_CONFIG.tableNames.archive}`; + + const projectsPromise = db.query(projectsQuery, [skipNum, resultLimit]); + const rowCountPromise = db.query(rowCountQuery); + + Promise.all([rowCountPromise, projectsPromise]) + .then(([[rowCount], projects]) => { + resolve({ + totalProjects: rowCount[Object.keys(rowCount)[0]], + projects: projects, + }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get a specific archive project by ID + */ +const getArchiveProject = (db, archiveId) => { + return new Promise((resolve, reject) => { + let query = `SELECT * FROM archive WHERE archive_id = ?`; + const params = [archiveId]; + + db.query(query, params) + .then((project) => { + resolve(project); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get archive project by slug + */ +const getArchiveFromSlug = (db, urlSlug) => { + return new Promise((resolve, reject) => { + let query = `SELECT * FROM archive WHERE url_slug=?`; + let params = [urlSlug]; + + db.query(query, params) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get archive entries for a project + */ +const getArchiveFromProject = (db, projectId) => { + return new Promise((resolve, reject) => { + let query = `SELECT * FROM archive WHERE archive.project_id=?`; + let params = [projectId]; + + db.query(query, params) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Search archive projects + */ +const searchForArchive = (db, resultLimit, offset, searchQuery, inactive) => { + return new Promise((resolve, reject) => { + let skipNum = offset * resultLimit; + let getProjectsQuery = ""; + let queryParams = []; + let getProjectsCount = ""; + let projectCountParams = []; + + const searchQueryParam = searchQuery || ""; + + if (inactive === "true") { + getProjectsQuery = `SELECT * FROM archive WHERE + archive.OID NOT IN ( + SELECT OID FROM archive + WHERE title like ? OR sponsor like ? OR members like ? OR coach like ? + OR keywords like ? OR synopsis like ? OR url_slug like ? + ORDER BY title, sponsor, members, coach, keywords, synopsis, url_slug + LIMIT ?) + AND (archive.title like ? OR archive.sponsor like ? OR archive.members like ? + OR archive.coach like ? OR archive.keywords like ? OR archive.synopsis like ? + OR archive.url_slug like ?) + ORDER BY archive.title, archive.sponsor, archive.members, archive.coach, + archive.keywords, archive.synopsis, archive.url_slug + LIMIT ?`; + + getProjectsCount = `SELECT COUNT(*) FROM archive + WHERE title like ? OR sponsor like ? OR members like ? OR coach like ? + OR keywords like ? OR synopsis like ? OR url_slug like ?`; + } else { + getProjectsQuery = `SELECT * FROM archive WHERE + archive.OID NOT IN ( + SELECT OID FROM archive + WHERE (title like ? OR sponsor like ? OR members like ? OR coach like ? + OR keywords like ? OR synopsis like ? OR url_slug like ?) AND inactive = '' + ORDER BY title, sponsor, members, coach, keywords, synopsis, url_slug + LIMIT ?) + AND ((archive.title like ? OR archive.sponsor like ? OR archive.members like ? + OR archive.coach like ? OR archive.keywords like ? OR archive.synopsis like ? + OR archive.url_slug like ?) AND inactive = '') + ORDER BY archive.title, archive.sponsor, archive.members, archive.coach, + archive.keywords, archive.synopsis, archive.url_slug + LIMIT ?`; + + getProjectsCount = `SELECT COUNT(*) FROM archive + WHERE (title like ? OR sponsor like ? OR members like ? OR coach like ? + OR keywords like ? OR synopsis like ? OR url_slug like ?) + AND (inactive = '' OR inactive IS NULL)`; + } + + queryParams = [ + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + skipNum || 0, + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + resultLimit || 0, + ]; + + projectCountParams = [ + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + ]; + + const projectPromise = db.query(getProjectsQuery, queryParams); + const projectCountPromise = db.query(getProjectsCount, projectCountParams); + + Promise.all([projectCountPromise, projectPromise]) + .then(([[projectCount], projectRows]) => { + resolve({ + projectCount: projectCount[Object.keys(projectCount)[0]], + projects: projectRows, + }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Edit archive entry (admin) + */ +const editArchive = (db, body, user) => { + return new Promise((resolve, reject) => { + const updateArchiveQuery = `UPDATE ${DB_CONFIG.tableNames.archive} + SET featured=?, outstanding=?, creative=?, priority=?, + title=?, project_id=?, team_name=?, + members=?, sponsor=?, coach=?, + poster_thumb=?, poster_full=?, archive_image=?, synopsis=?, + video=?, name=?, dept=?, + start_date=?, end_date=?, keywords=?, url_slug=?, inactive=?, locked=? + WHERE archive_id = ?`; + + const inactive = + body.inactive === "true" + ? moment().format(CONSTANTS.datetime_format) + : ""; + + const locked = + body.locked === "true" + ? user.fname + + " " + + user.lname + + " locked at " + + moment().format(CONSTANTS.datetime_format) + : ""; + + const updateArchiveParams = [ + checkBox(body.featured), + checkBox(body.outstanding), + checkBox(body.creative), + strToInt(body.priority), + body.title, + body.project_id, + body.team_name, + body.members, + body.sponsor, + body.coach, + body.poster_thumb, + body.poster_full, + body.archive_image, + body.synopsis, + body.video, + body.name, + body.dept, + body.start_date, + body.end_date, + body.keywords, + body.url_slug, + inactive, + locked, + body.archive_id, + ]; + + db.query(updateArchiveQuery, updateArchiveParams) + .then((response) => { + resolve(response); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Edit archive entry as student with file uploads + */ +const editArchiveStudent = (db, body, user, files) => { + return new Promise((resolve, reject) => { + const updateArchiveQuery = `UPDATE ${DB_CONFIG.tableNames.archive} + SET featured=?, outstanding=?, creative=?, priority=?, + title=?, project_id=?, team_name=?, + members=?, sponsor=?, coach=?, + poster_thumb=?, poster_full=?, archive_image=?, synopsis=?, + video=?, name=?, dept=?, + start_date=?, end_date=?, keywords=?, url_slug=?, inactive=?, locked=? + WHERE archive_id = ?`; + + const inactive = + body.inactive === "true" + ? moment().format(CONSTANTS.datetime_format) + : ""; + + const locked = + body.locked === "true" + ? user.fname + + " " + + user.lname + + " locked at " + + moment().format(CONSTANTS.datetime_format) + : ""; + + let files_uploaded = []; + let poster_full = ``; + let poster_thumb = ``; + let archive_image = ``; + let video = ``; + + if (!(files === undefined || files === null)) { + if (files.poster_full === undefined) { + poster_full = body.poster_full; + poster_thumb = body.poster_thumb; + } else { + if ( + files.poster_full.mimetype == "image/png" && + files.poster_full.size <= 30000000 + ) { + files.poster_full.name = body.url_slug + "-poster"; + poster_full = `${files.poster_full.name}`; + poster_thumb = poster_full; + let poster_URL = path.join( + __dirname, + `../../resource/archivePosters`, + ); + files_uploaded.push([files.poster_full, poster_URL]); + } else { + return reject(new Error("Invalid poster file")); + } + } + + if (files.archive_image === undefined) { + archive_image = body.archive_image; + } else { + if ( + files.archive_image.mimetype == "image/png" && + files.archive_image.size <= 30000000 + ) { + files.archive_image.name = body.url_slug + "-image"; + archive_image = `${files.archive_image.name}`; + let image_URL = path.join(__dirname, `../../resource/archiveImages`); + files_uploaded.push([files.archive_image, image_URL]); + } else { + return reject(new Error("Invalid archive image file")); + } + } + + if (files.video === undefined) { + video = body.video; + } else { + if ( + files.video.mimetype == "video/mp4" && + files.video.size <= 300000000 + ) { + files.video.name = body.url_slug + "-video"; + video = `${files.video.name}`; + let video_URL = path.join(__dirname, `../../resource/archiveVideos`); + files_uploaded.push([files.video, video_URL]); + } else { + return reject(new Error("Invalid video file")); + } + } + + for (let i = 0; i < files_uploaded.length; i++) { + fs.mkdirSync(files_uploaded[i][1], { recursive: true }); + files_uploaded[i][0].mv( + `${files_uploaded[i][1]}/${files_uploaded[i][0].name}`, + function (err) { + if (err) { + return reject(err); + } + }, + ); + } + } else { + poster_full = body.poster_full; + poster_thumb = body.poster_thumb; + archive_image = body.archive_image; + video = body.video; + } + + const updateArchiveParams = [ + checkBox(body.featured), + checkBox(body.outstanding), + checkBox(body.creative), + strToInt(body.priority), + body.title, + body.project_id, + body.team_name, + body.members, + body.sponsor, + body.coach, + poster_thumb, + poster_full, + archive_image, + body.synopsis, + video, + body.name, + body.dept, + body.start_date, + body.end_date, + body.keywords, + body.url_slug, + inactive, + locked, + body.archive_id, + ]; + + db.query(updateArchiveQuery, updateArchiveParams) + .then((response) => { + resolve(response); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Create archive entry as student with optional file uploads + */ +const createArchiveStudent = (db, body, user, files) => { + return new Promise((resolve, reject) => { + const inactive = + body.inactive === "true" + ? moment().format(CONSTANTS.datetime_format) + : ""; + + const locked = + body.locked === "true" + ? user.fname + + " " + + user.lname + + " locked at " + + moment().format(CONSTANTS.datetime_format) + : ""; + + const name = body.url_slug; + + let files_uploaded = []; + let poster_full = ``; + let poster_thumb = ``; + let archive_image = ``; + let video = ``; + + if (!(files === undefined || files === null)) { + if (files.poster_full === undefined) { + poster_full = body.poster_full; + poster_thumb = body.poster_thumb; + } else { + if ( + files.poster_full.mimetype == "image/png" && + files.poster_full.size <= 30000000 + ) { + files.poster_full.name = body.url_slug + "-poster"; + poster_full = `${files.poster_full.name}`; + poster_thumb = poster_full; + let poster_URL = path.join( + __dirname, + `../../resource/archivePosters`, + ); + files_uploaded.push([files.poster_full, poster_URL]); + } else { + return reject(new Error("Invalid poster file")); + } + } + + if (files.archive_image === undefined) { + archive_image = body.archive_image; + } else { + if ( + files.archive_image.mimetype == "image/png" && + files.archive_image.size <= 30000000 + ) { + files.archive_image.name = body.url_slug + "-image"; + archive_image = `${files.archive_image.name}`; + let image_URL = path.join(__dirname, `../../resource/archiveImages`); + files_uploaded.push([files.archive_image, image_URL]); + } else { + return reject(new Error("Invalid archive image file")); + } + } + + if (files.video === undefined) { + video = body.video; + } else { + if ( + files.video.mimetype == "video/mp4" && + files.video.size <= 300000000 + ) { + files.video.name = body.url_slug + "-video"; + video = `${files.video.name}`; + let video_URL = path.join(__dirname, `../../resource/archiveVideos`); + files_uploaded.push([files.video, video_URL]); + } else { + return reject(new Error("Invalid video file")); + } + } + + for (let i = 0; i < files_uploaded.length; i++) { + fs.mkdirSync(files_uploaded[i][1], { recursive: true }); + if ( + fs.existsSync(`${files_uploaded[i][1]}/${files_uploaded[i][0].name}`) + ) { + fs.unlinkSync(`${files_uploaded[i][1]}/${files_uploaded[i][0].name}`); + } + files_uploaded[i][0].mv( + `${files_uploaded[i][1]}/${files_uploaded[i][0].name}`, + function (err) { + if (err) { + return reject(err); + } + }, + ); + } + } else { + poster_full = body.poster_full; + poster_thumb = body.poster_thumb; + archive_image = body.archive_image; + video = body.video; + } + + const updateArchiveQuery = `INSERT INTO ${DB_CONFIG.tableNames.archive}(featured, outstanding, creative, + priority, title, project_id, team_name, members, sponsor, coach, poster_thumb, + poster_full, archive_image, synopsis, video, name, dept, start_date, end_date, + keywords, url_slug, inactive, locked) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + + const updateArchiveParams = [ + checkBox(body.featured), + checkBox(body.outstanding), + checkBox(body.creative), + strToInt(body.priority), + body.title, + body.project_id, + body.team_name, + body.members, + body.sponsor, + body.coach, + poster_thumb, + poster_full, + archive_image, + body.synopsis, + video, + name, + body.dept, + body.start_date, + body.end_date, + body.keywords, + body.url_slug, + inactive, + locked, + ]; + + db.query(updateArchiveQuery, updateArchiveParams) + .then((response) => { + resolve(response); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all archive entries (admin only) + */ +const getArchive = (db) => { + return new Promise((resolve, reject) => { + let getArchiveQuery = `SELECT * FROM archive`; + db.query(getArchiveQuery) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get archive poster file + */ +const getArchivePoster = (fileName) => { + return path.join(__dirname, "../../resource/archivePosters/" + fileName); +}; + +/** + * Get archive video file + */ +const getArchiveVideo = (fileName) => { + return path.join(__dirname, "../../resource/archiveVideos/" + fileName); +}; + +/** + * Get archive image file + */ +const getArchiveImage = (fileName) => { + return path.join(__dirname, "../../resource/archiveImages/" + fileName); +}; + +module.exports = { + createArchive, + getActiveArchiveProjects, + getArchiveProjects, + getArchiveProject, + getArchiveFromSlug, + getArchiveFromProject, + searchForArchive, + editArchive, + editArchiveStudent, + createArchiveStudent, + getArchive, + getArchivePoster, + getArchiveVideo, + getArchiveImage, +}; diff --git a/server/server/routing/functions/dashboard-func.js b/server/server/routing/functions/dashboard-func.js new file mode 100644 index 00000000..a192bfb7 --- /dev/null +++ b/server/server/routing/functions/dashboard-func.js @@ -0,0 +1,289 @@ +/** + * Get additional user info + */ +const getAdditionalInfo = (db, systemId) => { + return new Promise((resolve, reject) => { + const query = `SELECT json_extract(profile_info, '$.additional_info') AS additional_info + FROM users WHERE system_id = ?`; + + db.query(query, [systemId]) + .then((result) => { + if (result.length === 0) { + reject(new Error("User not found")); + return; + } + resolve(result[0]); + }) + .catch((err) => { + reject(new Error("Database query failed: " + err.message)); + }); + }); +}; + +/** + * Update additional user info + */ +const editAdditionalInfo = (db, systemId, additionalInfo) => { + return new Promise((resolve, reject) => { + const query = `UPDATE users + SET profile_info = json_set(profile_info, '$.additional_info', ?) + WHERE system_id = ?`; + + db.query(query, [additionalInfo, systemId]) + .then(() => { + resolve({ message: "Additional info updated successfully" }); + }) + .catch((err) => { + reject(new Error("Database update failed: " + err.message)); + }); + }); +}; + +/** + * Set dark mode preference + */ +const setDarkMode = (db, systemId, darkMode) => { + return new Promise((resolve, reject) => { + const query = `UPDATE users + SET profile_info = json_set(profile_info, '$.dark_mode', ?) + WHERE system_id = ?`; + + db.query(query, [darkMode, systemId]) + .then(() => { + resolve({ message: "Dark mode preference updated successfully" }); + }) + .catch((err) => { + reject(new Error("Database update failed: " + err.message)); + }); + }); +}; + +/** + * Get dark mode preference + */ +const getDarkMode = (db, systemId) => { + return new Promise((resolve, reject) => { + const query = `SELECT JSON_EXTRACT(profile_info, '$.dark_mode') AS dark_mode + FROM users + WHERE system_id = ?`; + + db.query(query, [systemId]) + .then((result) => { + if (result.length === 0) { + reject(new Error("User not found")); + return; + } + + const darkModeRaw = result[0].dark_mode; + const dark_mode = Boolean(darkModeRaw); + + resolve({ dark_mode }); + }) + .catch((err) => { + reject(new Error("Database query failed: " + err.message)); + }); + }); +}; + +/** + * Get peer evaluations + */ +const getPeerEvals = (db, semesterNumber) => { + return new Promise((resolve, reject) => { + let query = `SELECT action_id FROM actions WHERE action_target = 'peer_evaluation'`; + let queryParams = []; + + if (semesterNumber) { + query += ` AND semester = ?`; + queryParams.push(semesterNumber); + } + + db.query(query, queryParams) + .then((values) => { + const actionIds = values.map((row) => row.action_id); + + if (actionIds.length === 0) { + resolve([]); + return; + } + + const getPeerEvalLogsQuery = `SELECT action_log.*, users.fname, users.lname, users.type + FROM action_log + LEFT JOIN users ON action_log.system_id = users.system_id + WHERE action_template IN (${actionIds.join(",")}) + ORDER BY submission_datetime DESC`; + + return db.query(getPeerEvalLogsQuery); + }) + .then((logs) => { + resolve(logs); + }) + .catch((err) => { + reject(new Error("Error fetching peer evaluations: " + err.message)); + }); + }); +}; + +/** + * Set gantt view preference + */ +const setGanttView = (db, systemId, ganttView) => { + return new Promise((resolve, reject) => { + const query = `UPDATE users + SET profile_info = json_set(profile_info, '$.gantt_view', ?) + WHERE system_id = ?`; + + db.query(query, [ganttView, systemId]) + .then(() => { + resolve({ message: "Gantt view preference updated successfully" }); + }) + .catch((err) => { + reject(new Error("Database update failed: " + err.message)); + }); + }); +}; + +/** + * Get gantt view preference + */ +const getGanttView = (db, systemId) => { + return new Promise((resolve, reject) => { + const query = `SELECT JSON_EXTRACT(profile_info, '$.gantt_view') AS gantt_view + FROM users + WHERE system_id = ?`; + + db.query(query, [systemId]) + .then((result) => { + if (result.length === 0) { + reject(new Error("User not found")); + return; + } + + const ganttViewRaw = result[0].gantt_view; + const gantt_view = + ganttViewRaw === true || + ganttViewRaw === "true" || + ganttViewRaw === 1 || + ganttViewRaw === "1"; + + resolve({ gantt_view }); + }) + .catch((err) => { + reject(new Error("Database query failed: " + err.message)); + }); + }); +}; + +/** + * Set calendar view preference + */ +const setCalendarView = (db, systemId, calendarView) => { + return new Promise((resolve, reject) => { + const query = `UPDATE users + SET profile_info = json_set(profile_info, '$.calendar_view', ?) + WHERE system_id = ?`; + + db.query(query, [calendarView, systemId]) + .then(() => { + resolve({ message: "Calendar view preference updated successfully" }); + }) + .catch((err) => { + reject(new Error("Database update failed: " + err.message)); + }); + }); +}; + +/** + * Get calendar view preference + */ +const getCalendarView = (db, systemId) => { + return new Promise((resolve, reject) => { + const query = `SELECT JSON_EXTRACT(profile_info, '$.calendar_view') AS calendar_view + FROM users + WHERE system_id = ?`; + + db.query(query, [systemId]) + .then((result) => { + if (result.length === 0) { + reject(new Error("User not found")); + return; + } + + const calendarViewRaw = result[0].calendar_view; + const calendar_view = + calendarViewRaw === true || + calendarViewRaw === "true" || + calendarViewRaw === 1 || + calendarViewRaw === "1"; + + resolve({ calendar_view }); + }) + .catch((err) => { + reject(new Error("Database query failed: " + err.message)); + }); + }); +}; + +/** + * Set milestone view preference + */ +const setMilestoneView = (db, systemId, milestoneView) => { + return new Promise((resolve, reject) => { + const query = `UPDATE users + SET profile_info = json_set(profile_info, '$.milestone_view', ?) + WHERE system_id = ?`; + + db.query(query, [milestoneView, systemId]) + .then(() => { + resolve({ message: "Milestone view preference updated successfully" }); + }) + .catch((err) => { + reject(new Error("Database update failed: " + err.message)); + }); + }); +}; + +/** + * Get milestone view preference + */ +const getMilestoneView = (db, systemId) => { + return new Promise((resolve, reject) => { + const query = `SELECT JSON_EXTRACT(profile_info, '$.milestone_view') AS milestone_view + FROM users + WHERE system_id = ?`; + + db.query(query, [systemId]) + .then((result) => { + if (result.length === 0) { + reject(new Error("User not found")); + return; + } + + const milestoneViewRaw = result[0].milestone_view; + const milestone_view = + milestoneViewRaw === true || + milestoneViewRaw === "true" || + milestoneViewRaw === 1 || + milestoneViewRaw === "1"; + + resolve({ milestone_view }); + }) + .catch((err) => { + reject(new Error("Database query failed: " + err.message)); + }); + }); +}; + +module.exports = { + getAdditionalInfo, + editAdditionalInfo, + setDarkMode, + getDarkMode, + getPeerEvals, + setGanttView, + getGanttView, + setCalendarView, + getCalendarView, + setMilestoneView, + getMilestoneView, +}; diff --git a/server/server/routing/functions/dev-only-func.js b/server/server/routing/functions/dev-only-func.js new file mode 100644 index 00000000..e29569f5 --- /dev/null +++ b/server/server/routing/functions/dev-only-func.js @@ -0,0 +1,44 @@ +const CONSTANTS = require("../consts"); +const redeployDatabase = require("../../db_setup"); + +/** + * Get all users for login (development only) + */ +const devOnlyGetAllUsersForLogin = (db) => { + return new Promise((resolve, reject) => { + const query = `SELECT ${CONSTANTS.SIGN_IN_SELECT_ATTRIBUTES} FROM users`; + db.query(query) + .then((users) => { + resolve(users); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Redeploy database (development only) + */ +const devOnlyRedeployDatabase = () => { + return new Promise(async (resolve, reject) => { + try { + await redeployDatabase(); + resolve({ + success: true, + message: "Database redeployed successfully", + }); + } catch (error) { + reject({ + success: false, + message: "Failed to redeploy database", + error: error.message, + }); + } + }); +}; + +module.exports = { + devOnlyGetAllUsersForLogin, + devOnlyRedeployDatabase, +}; diff --git a/server/server/routing/functions/error_logs-func.js b/server/server/routing/functions/error_logs-func.js new file mode 100644 index 00000000..e7dd32d9 --- /dev/null +++ b/server/server/routing/functions/error_logs-func.js @@ -0,0 +1,30 @@ +const DB_CONFIG = require("../database/db_config"); + +/** + * Get all error logs with pagination + */ +const getErrorLogs = (db, resultLimit, offset) => { + return new Promise((resolve, reject) => { + let skipNum = offset * resultLimit; + let logsQuery = `SELECT * FROM ${DB_CONFIG.tableNames.action_log} ORDER BY timestamp DESC LIMIT ? OFFSET ?`; + let countQuery = `SELECT COUNT(*) FROM ${DB_CONFIG.tableNames.action_log}`; + + const logsPromise = db.query(logsQuery, [resultLimit, skipNum]); + const countPromise = db.query(countQuery); + + Promise.all([countPromise, logsPromise]) + .then(([[count], logs]) => { + resolve({ + totalLogs: count[Object.keys(count)[0]], + logs: logs, + }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +module.exports = { + getErrorLogs, +}; diff --git a/server/server/routing/functions/files-func.js b/server/server/routing/functions/files-func.js new file mode 100644 index 00000000..b85fec2b --- /dev/null +++ b/server/server/routing/functions/files-func.js @@ -0,0 +1,278 @@ +const fs = require("fs"); +const path = require("path"); +const fse = require("fs-extra"); + +/** + * Upload files (admin only) + */ +const uploadFiles = (files, uploadPath) => { + return new Promise((resolve, reject) => { + let filesUploaded = []; + + // Attachment Handling + if (files && files.files) { + // If there is only one attachment, then it does not come as a list + if (files.files.length === undefined) { + files.files = [files.files]; + } + + const formattedPath = `resource/${uploadPath}`; + const baseURL = path.join(__dirname, `../../${formattedPath}`); + + //If directory exists, it won't make one, otherwise it will based on the baseUrl + fs.mkdirSync(baseURL, { recursive: true }); + for (let x = 0; x < files.files.length; x++) { + files.files[x].mv(`${baseURL}/${files.files[x].name}`, function (err) { + if (err) { + reject(err); + } + }); + filesUploaded.push( + `${process.env.BASE_URL}/${formattedPath}/${files.files[x].name}`, + ); + } + } + + resolve({ msg: "Success!", filesUploaded: filesUploaded }); + }); +}; + +/** + * Upload files for student with archive update + */ +const uploadFilesStudent = (db, files, uploadPath, archiveId, column) => { + return new Promise((resolve, reject) => { + let filesUploaded = []; + + // Attachment Handling + if (files && files.files) { + // If there is only one attachment, then it does not come as a list + if (files.files.length === undefined) { + files.files = [files.files]; + } + + const formattedPath = `resource/${uploadPath}`; + const baseURL = path.join(__dirname, `../../${formattedPath}`); + + //If directory exists, it won't make one, otherwise it will based on the baseUrl + fs.mkdirSync(baseURL, { recursive: true }); + for (let x = 0; x < files.files.length; x++) { + files.files[x].mv(`${baseURL}/${files.files[x].name}`, function (err) { + if (err) { + reject(err); + } + }); + filesUploaded.push( + `${process.env.BASE_URL}/${formattedPath}/${files.files[x].name}`, + ); + + let fileName = files.files[x].name; + let pathString = uploadPath; + pathString = pathString.split("/"); + pathString.shift(); + pathString = '"' + pathString.join("/") + "/" + fileName + '"'; + let query = `UPDATE archive + SET ${column} = ${pathString} + WHERE archive_id = ${archiveId}`; + db.query(query).catch((err) => { + reject(err); + }); + } + } + + resolve({ msg: "Success!", filesUploaded: filesUploaded }); + }); +}; + +/** + * Create a new directory + */ +const createDirectory = (directoryPath) => { + return new Promise((resolve, reject) => { + const formattedPath = + directoryPath === "" ? `resource/` : `resource/${directoryPath}`; + const baseURL = path.join(__dirname, `../../${formattedPath}`); + if (!fs.existsSync(baseURL)) { + fs.mkdirSync(baseURL, { recursive: true }); + resolve({ msg: "Success!" }); + } else { + reject(new Error("Directory already exists")); + } + }); +}; + +/** + * Rename a directory or file + */ +const renameDirectoryOrFile = (oldPath, newPath) => { + return new Promise((resolve, reject) => { + const formattedOldPath = + oldPath === "" ? `resource/` : `resource/${oldPath}`; + const formattedNewPath = + newPath === "" ? `resource/` : `resource/${newPath}`; + const baseURLOld = path.join(__dirname, `../../${formattedOldPath}`); + const baseURLNew = path.join(__dirname, `../../${formattedNewPath}`); + + // New path already exists, so we can't rename + if (fs.existsSync(baseURLNew)) { + reject(new Error("Target path already exists")); + return; + } + + // Copy all files from old directory to new directory + if (fs.lstatSync(baseURLOld).isDirectory()) { + fse.copySync(baseURLOld, baseURLNew); + fs.rmdirSync(baseURLOld, { recursive: true }); + resolve({ msg: "Success!" }); + // Rename file + } else if (fs.lstatSync(baseURLOld).isFile()) { + fs.renameSync(baseURLOld, baseURLNew); + resolve({ msg: "Success!" }); + } + }); +}; + +/** + * Get files in a directory + */ +const getFiles = (directoryPath) => { + return new Promise((resolve, reject) => { + let fileData = []; + const formattedPath = + directoryPath === "" ? `resource/` : `resource/${directoryPath}`; + const baseURL = path.join(__dirname, `../../${formattedPath}`); + + if (fs.existsSync(baseURL)) { + // Get the files in the directory + fs.readdir(baseURL, function (err, files) { + if (err) { + console.error(`Error reading directory ${baseURL}:`, err); + // Return empty array instead of throwing error for missing directories + resolve(fileData); + return; + } + + try { + const info = fs.statSync(baseURL); + files.forEach(function (file) { + try { + // Only files have sizes, directories do not. Send file size if it is a file + const fileInfo = fs.statSync(path.join(baseURL, file)); + if (fileInfo.isFile()) { + fileData.push({ + file: file, + size: fileInfo.size, + lastModified: fileInfo.ctime, + }); + } else { + fileData.push({ + file: file, + size: 0, + lastModified: info.ctime, + }); + } + } catch (fileErr) { + console.error(`Error processing file ${file}:`, fileErr); + // Skip files that can't be processed + } + }); + } catch (statErr) { + console.error( + `Error getting directory stats for ${baseURL}:`, + statErr, + ); + } + + resolve(fileData); + }); + } else { + console.log(`Directory does not exist: ${baseURL}`); + resolve(fileData); + } + }); +}; + +/** + * Get project files + */ +const getProjectFiles = () => { + return new Promise((resolve, reject) => { + let fileData = []; + const formattedPath = `resource/`; + const baseURL = path.join(__dirname, `../../${formattedPath}`); + fs.mkdirSync(baseURL, { recursive: true }); + + // Get the files in the directory + fs.readdir(baseURL, function (err, files) { + if (err) { + reject(err); + return; + } + const info = fs.statSync(baseURL); + files.forEach(function (file) { + // Only files have sizes, directories do not. Send file size if it is a file + const fileInfo = fs.statSync(baseURL + file); + if (fileInfo.isFile()) { + fileData.push({ + file: file, + size: fileInfo.size, + lastModified: fileInfo.ctime, + }); + } else { + fileData.push({ + file: file, + size: 0, + lastModified: info.ctime, + }); + } + }); + resolve(fileData); + }); + }); +}; + +/** + * Remove a file + */ +const removeFile = (filePath) => { + return new Promise((resolve, reject) => { + const formattedPath = `resource/${filePath}`; + const baseURL = path.join(__dirname, `../../${formattedPath}`); + + if (fs.existsSync(baseURL)) { + fs.unlinkSync(baseURL); + resolve({ msg: "Success!" }); + } else { + reject(new Error("File does not exist")); + } + }); +}; + +/** + * Remove a directory + */ +const removeDirectory = (directoryPath) => { + return new Promise((resolve, reject) => { + const formattedPath = + directoryPath === "" ? `resource/` : `resource/${directoryPath}`; + const baseURL = path.join(__dirname, `../../${formattedPath}`); + + if (fs.existsSync(baseURL)) { + fs.rmdirSync(baseURL, { recursive: true }); + resolve({ msg: "Success!" }); + } else { + reject(new Error("Directory does not exist")); + } + }); +}; + +module.exports = { + uploadFiles, + uploadFilesStudent, + createDirectory, + renameDirectoryOrFile, + getFiles, + getProjectFiles, + removeFile, + removeDirectory, +}; diff --git a/server/server/routing/functions/projects-func.js b/server/server/routing/functions/projects-func.js new file mode 100644 index 00000000..3ab062db --- /dev/null +++ b/server/server/routing/functions/projects-func.js @@ -0,0 +1,378 @@ +const DB_CONFIG = require("../database/db_config"); + +/** + * Get all active projects + */ +const getActiveProjects = (db) => { + return new Promise((resolve, reject) => { + let query = ` + SELECT * + FROM projects + LEFT JOIN semester_group + ON projects.semester = semester_group.semester_id + WHERE projects.semester IS NOT NULL + `; + + db.query(query) + .then((projects) => { + resolve(projects); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all active coaches + */ +const getActiveCoaches = (db, coachRole) => { + return new Promise((resolve, reject) => { + const sql = `SELECT * FROM users WHERE type = ? AND active = ''`; + + db.query(sql, [coachRole]) + .then((coaches) => { + resolve(coaches); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get coaches for a specific project + */ +const getProjectCoaches = (db, projectId) => { + return new Promise((resolve, reject) => { + const query = `SELECT users.* FROM users + LEFT JOIN project_coaches ON project_coaches.coach_id = users.system_id + WHERE project_coaches.project_id = ?`; + + db.query(query, [projectId]) + .then((coaches) => { + resolve(coaches); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get students for a specific project + */ +const getProjectStudents = (db, projectId) => { + return new Promise((resolve, reject) => { + const query = "SELECT * FROM users WHERE users.project = ?"; + + db.query(query, [projectId]) + .then((students) => { + resolve(students); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get student names for a project (excluding a specific student) + */ +const getProjectStudentNames = (db, projectId, excludeSystemId) => { + return new Promise((resolve, reject) => { + const query = + "SELECT fname,lname FROM users WHERE users.project = ? and users.system_id != ?"; + + db.query(query, [projectId, excludeSystemId]) + .then((students) => { + resolve(students); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all coach info with their projects + */ +const selectAllCoachInfo = (db, coachRole) => { + return new Promise((resolve, reject) => { + const query = ` + SELECT users.system_id, + users.fname, + users.lname, + users.email, + users.semester_group, + ( + SELECT "[" || group_concat( + "{" || + """title""" || ":" || """" || COALESCE(projects.display_name, projects.title) || """" || "," || + """semester_id""" || ":" || """" || projects.semester || """" || "," || + """project_id""" || ":" || """" || projects.project_id || """" || "," || + """organization""" || ":" || """" || projects.organization || """" || "," || + """status""" || ":" || """" || projects.status || """" || + "}" + ) || "]" + FROM project_coaches + LEFT JOIN projects ON projects.project_id = project_coaches.project_id + WHERE project_coaches.coach_id = users.system_id + ) projects + FROM users + WHERE users.type = ? + `; + + db.query(query, [coachRole]) + .then((coaches) => { + resolve(coaches); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all projects + */ +const getProjects = (db) => { + return new Promise((resolve, reject) => { + const query = "SELECT * from projects"; + + db.query(query) + .then((projects) => { + resolve(projects); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get candidate projects + */ +const getCandidateProjects = (db) => { + return new Promise((resolve, reject) => { + const query = "SELECT * from projects WHERE projects.status = 'candidate';"; + + db.query(query) + .then((projects) => { + resolve(projects); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get projects for a user based on their role + */ +const getMyProjects = (db, userType, userId) => { + return new Promise((resolve, reject) => { + let query; + let params; + + switch (userType) { + case "coach": + query = `SELECT projects.* + FROM projects + INNER JOIN project_coaches + ON (projects.project_id = project_coaches.project_id AND project_coaches.coach_id = ?)`; + params = [userId]; + break; + case "student": + query = `SELECT users.system_id, users.semester_group, projects.* + FROM users + INNER JOIN projects + ON users.system_id = ? AND projects.project_id = users.project`; + params = [userId]; + break; + case "admin": + query = + "SELECT * FROM projects WHERE projects.status NOT IN ('completed', 'rejected', 'archive')"; + params = []; + break; + default: + reject(new Error("Invalid user type")); + return; + } + + db.query(query, params) + .then((projects) => { + resolve(projects); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get semester projects + */ +const getSemesterProjects = (db, userType, userId) => { + return new Promise((resolve, reject) => { + let query; + let params; + + switch (userType) { + case "coach": + query = ` + SELECT projects.* + FROM projects + WHERE projects.semester IN + (SELECT projects.semester + FROM projects + INNER JOIN project_coaches + ON (projects.project_id = project_coaches.project_id AND project_coaches.coach_id = ?)) + `; + params = [userId]; + break; + case "student": + query = `SELECT users.system_id, projects.* + FROM users + INNER JOIN projects + ON users.system_id = ? AND projects.semester = users.semester_group`; + params = [userId]; + break; + case "admin": + query = + "SELECT * FROM projects WHERE projects.status NOT IN ('in progress', 'completed', 'rejected', 'archive')"; + params = []; + break; + default: + reject(new Error("Invalid user type")); + return; + } + + db.query(query, params) + .then((projects) => { + resolve(projects); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get project dates for a semester + */ +const getProjectDates = (db, semesterId) => { + return new Promise((resolve, reject) => { + const query = `SELECT start_date, end_date FROM semester_group WHERE semester_id = ?`; + + db.query(query, [semesterId]) + .then((dates) => { + resolve(dates); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Edit project details and coaches + */ +const editProject = (db, body) => { + return new Promise((resolve, reject) => { + const updateProjectSql = `UPDATE ${DB_CONFIG.tableNames.senior_projects} + SET status=?, title=?, display_name=?, organization=?, primary_contact=?, contact_email=?, contact_phone=?, + background_info=?, project_description=?, project_scope=?, project_challenges=?, + sponsor_provided_resources=?, project_search_keywords=?, constraints_assumptions=?, sponsor_deliverables=?, + proprietary_info=?, sponsor_alternate_time=?, sponsor_avail_checked=?, project_agreements_checked=?, assignment_of_rights=?, + team_name=?, poster=?, video=?, website=?, synopsis=?, sponsor=?, semester=? + WHERE project_id = ?`; + + const updateProjectParams = [ + body.status, + body.title, + body.display_name ? body.display_name : null, + body.organization, + body.primary_contact, + body.contact_email, + body.contact_phone, + body.background_info, + body.project_description, + body.project_scope, + body.project_challenges, + body.sponsor_provided_resources, + body.project_search_keywords, + body.constraints_assumptions, + body.sponsor_deliverables, + body.proprietary_info, + body.sponsor_alternate_time, + body.sponsor_avail_checked, + body.project_agreements_checked, + body.assignment_of_rights, + body.team_name, + body.poster, + body.video, + body.website, + body.synopsis, + body.sponsor, + body.semester || null, + body.project_id, + ]; + + const insertValues = body.projectCoaches + .split(",") + .map((coachId) => ` ('${body.project_id}', '${coachId}')`); + const deleteValues = body.projectCoaches + .split(",") + .map((coachId) => `'${coachId}'`); + const updateCoachesSql = `INSERT OR IGNORE INTO '${DB_CONFIG.tableNames.project_coaches}' ('project_id', 'coach_id') VALUES ${insertValues};`; + const deleteCoachesSQL = `DELETE FROM '${DB_CONFIG.tableNames.project_coaches}' + WHERE project_coaches.project_id = '${body.project_id}' + AND project_coaches.coach_id NOT IN (${deleteValues});`; + + Promise.all([ + db.query(updateProjectSql, updateProjectParams), + db.query(updateCoachesSql), + db.query(deleteCoachesSQL), + ]) + .then(() => { + resolve({ success: true }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Update proposal status + */ +const updateProposalStatus = (db, projectId, status) => { + return new Promise((resolve, reject) => { + const query = `UPDATE ${DB_CONFIG.tableNames.senior_projects} SET status = ? WHERE project_id = ?`; + + db.query(query, [status, projectId]) + .then(() => { + resolve({ success: true }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +module.exports = { + getActiveProjects, + getActiveCoaches, + getProjectCoaches, + getProjectStudents, + getProjectStudentNames, + selectAllCoachInfo, + getProjects, + getCandidateProjects, + getMyProjects, + getSemesterProjects, + getProjectDates, + editProject, + updateProposalStatus, +}; diff --git a/server/server/routing/functions/proposals-func.js b/server/server/routing/functions/proposals-func.js new file mode 100644 index 00000000..4d0a576b --- /dev/null +++ b/server/server/routing/functions/proposals-func.js @@ -0,0 +1,237 @@ +const fs = require("fs"); +const path = require("path"); +const PDFDoc = require("pdfkit"); +const he = require("he"); +const { convert } = require("html-to-text"); +const { nanoid } = require("nanoid"); +const moment = require("moment"); +const DB_CONFIG = require("../database/db_config"); +const CONFIG = require("../config/config"); + +/** + * Get list of proposal PDF file names + */ +const getProposalPdfNames = () => { + return new Promise((resolve, reject) => { + fs.readdir(path.join(__dirname, "../proposal_docs"), function (err, files) { + if (err) { + reject(err); + return; + } + let fileLinks = []; + files.forEach(function (file) { + fileLinks.push(file.toString()); + }); + resolve(fileLinks); + }); + }); +}; + +/** + * Get proposal PDF file + */ +const getProposalPdf = (projectId) => { + return new Promise((resolve, reject) => { + if (projectId) { + let safeProjectId = projectId.replace(/\\|\//g, ""); + const filePath = path.join( + __dirname, + `../proposal_docs/${safeProjectId}.pdf`, + ); + resolve(filePath); + } else { + reject(new Error("File not found")); + } + }); +}; + +/** + * Get list of proposal attachment file names + */ +const getProposalAttachmentNames = (projectId) => { + return new Promise((resolve, reject) => { + if (projectId) { + let safeProjectId = projectId.replace(/\\|\//g, ""); + fs.readdir( + path.join( + __dirname, + `./server/sponsor_proposal_files/${safeProjectId}`, + ), + function (err, files) { + if (err) { + reject(err); + return; + } + let fileLinks = []; + files.forEach(function (file) { + fileLinks.push(file.toString()); + }); + resolve(fileLinks); + }, + ); + } else { + reject(new Error("Bad request")); + } + }); +}; + +/** + * Get proposal attachment file + */ +const getProposalAttachment = (projectId, fileName) => { + return new Promise((resolve, reject) => { + if (projectId && fileName) { + let safeProjectId = projectId.replace(/\\|\//g, ""); + let safeName = fileName.replace(/\\|\//g, ""); + const filePath = path.join( + __dirname, + `../sponsor_proposal_files/${safeProjectId}/${safeName}`, + ); + resolve(filePath); + } else { + reject(new Error("File not found")); + } + }); +}; + +/** + * Submit a new proposal + */ +const submitProposal = (db, body, files) => { + return new Promise((resolve, reject) => { + let date = new Date(); + let timeString = `${date.getFullYear()}-${date.getUTCMonth()}-${date.getDate()}`; + const projectId = `${timeString}_${nanoid()}`; + + let filenamesCSV = ""; + // Attachment Handling + if (files && files.attachments) { + // If there is only one attachment, then it does not come as a list + if (files.attachments.length === undefined) { + files.attachments = [files.attachments]; + } + + if (files.attachments.length > 5) { + reject(new Error("Maximum of 5 files allowed")); + return; + } + + const baseURL = path.join( + __dirname, + `../sponsor_proposal_files/${projectId}`, + ); + + fs.mkdirSync(baseURL, { recursive: true }); + + for (let x = 0; x < files.attachments.length; x++) { + if (files.attachments[x].size > 15 * 1024 * 1024) { + // 15mb limit exceeded + reject(new Error("File size limit exceeded")); + return; + } + if ( + !CONFIG.accepted_file_types.includes( + path.extname(files.attachments[x].name), + ) + ) { + reject(new Error("file type not accepted")); + return; + } + + // Append the file name to the CSV string, begin with a comma if x is not 0 + filenamesCSV += + x === 0 + ? `${files.attachments[x].name}` + : `, ${files.attachments[x].name}`; + + files.attachments[x].mv( + `${baseURL}/${files.attachments[x].name}`, + function (err) { + if (err) { + reject(err); + } + }, + ); + } + } + + const sql = `INSERT INTO ${DB_CONFIG.tableNames.senior_projects} + (project_id, status, title, organization, primary_contact, contact_email, contact_phone, attachments, + background_info, project_description, project_scope, project_challenges, + sponsor_provided_resources, constraints_assumptions, sponsor_deliverables, + proprietary_info, sponsor_alternate_time, sponsor_avail_checked, project_agreements_checked, assignment_of_rights) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`; + + const params = [ + projectId, + "submitted", + body.title.substring(0, 50), + body.organization, + body.primary_contact, + body.contact_email, + body.contact_phone, + filenamesCSV, + body.background_info, + body.project_description, + body.project_scope, + body.project_challenges, + body.sponsor_provided_resources, + body.constraints_assumptions, + body.sponsor_deliverables, + body.proprietary_info, + body.sponsor_alternate_time, + body.sponsor_avail_checked, + body.project_agreements_checked, + body.assignment_of_rights, + ]; + + db.query(sql, params) + .then(() => { + // Generate PDF + let doc = new PDFDoc(); + const baseURL = path.join(__dirname, `../proposal_docs/`); + fs.mkdirSync(baseURL, { recursive: true }); + doc.pipe(fs.createWriteStream(`${baseURL}/${projectId}.pdf`)); + + doc.font("Times-Roman"); + + for (let key of Object.keys(DB_CONFIG.senior_project_proposal_keys)) { + doc + .fill("blue") + .fontSize(16) + .text(DB_CONFIG.senior_project_proposal_keys[key]), + { + underline: true, + }; + doc + .fontSize(12) + .fill("black") + .text(convert(he.decode(body[key] || ""))); + doc.moveDown(); + doc.save(); + } + + doc.fill("blue").fontSize(16).text("Attachments"), + { + underline: true, + }; + doc.fontSize(12).fill("black").text(filenamesCSV); + doc.moveDown(); + doc.save(); + + doc.end(); + resolve(); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +module.exports = { + getProposalPdfNames, + getProposalPdf, + getProposalAttachmentNames, + getProposalAttachment, + submitProposal, +}; diff --git a/server/server/routing/functions/semester-func.js b/server/server/routing/functions/semester-func.js new file mode 100644 index 00000000..ae22cc71 --- /dev/null +++ b/server/server/routing/functions/semester-func.js @@ -0,0 +1,135 @@ +const { ROLES } = require("../consts"); +const ACTION_TARGETS = { + ADMIN: "admin", + COACH: "coach", + INDIVIDUAL: "individual", + TEAM: "team", + PEER_EVALUATION: "peer_evaluation", + COACH_ANNOUNCEMENT: "coach_announcement", + STUDENT_ANNOUNCEMENT: "student_announcement", +}; + +/** + * Get all semesters + */ +const getSemesters = (db) => { + return new Promise((resolve, reject) => { + const query = ` + SELECT * + FROM semester_group + ORDER BY end_date, start_date, name + `; + db.query(query) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get semester announcements + */ +const getSemesterAnnouncements = (db, user, semester) => { + return new Promise((resolve, reject) => { + let filter = ""; + if (user.type === ROLES.STUDENT) { + // req.query.semester comes in as a string and req.user.semester_group is a number so convert both to strings to compare them. + if (`${semester}` !== `${user.semester_group}`) { + reject( + new Error( + "Students can not access announcements that are not for your project", + ), + ); + return; + } + + filter = `AND actions.action_target IS NOT '${ACTION_TARGETS.COACH_ANNOUNCEMENT}'`; + // Note: Since we only do this check for students, coaches can technically hack the request to see announcements for other semesters. + // Unfortunately, coaches don't inherently have a semester like students do + // and 1am Kevin can't think of another way of ensuring that a coach isn't lying to us about their semester ...but idk what they would gain form doing that sooo ima just leave it for now + } + + //ToDo: make sure that the dates don't screw things up because of GMT i.e. it becomes tomorrow in GMT before it becomes tomorrow at the server's location + const query = ` + SELECT action_title, action_id, start_date, due_date, semester, action_target, date_deleted, page_html + FROM actions + WHERE actions.date_deleted = '' AND actions.semester = ? + AND (actions.action_target IN ('${ACTION_TARGETS.COACH_ANNOUNCEMENT}', '${ACTION_TARGETS.STUDENT_ANNOUNCEMENT}') AND actions.start_date <= date('now') AND actions.due_date >= date('now')) + ${filter} + ORDER BY actions.due_date ASC + `; + + db.query(query, [semester]) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Edit an existing semester + */ +const editSemester = (db, body) => { + return new Promise((resolve, reject) => { + const query = ` + UPDATE semester_group + SET name = ?, + dept = ?, + start_date = ?, + end_date = ? + WHERE semester_id = ? + `; + + const params = [ + body.name, + body.dept, + body.start_date, + body.end_date, + body.semester_id, + ]; + + db.query(query, params) + .then(() => { + resolve({ success: true }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Create a new semester + */ +const createSemester = (db, body) => { + return new Promise((resolve, reject) => { + const query = ` + INSERT INTO semester_group + (name, dept, start_date, end_date) + VALUES (?,?,?,?) + `; + + const params = [body.name, body.dept, body.start_date, body.end_date]; + + db.query(query, params) + .then(() => { + resolve({ success: true }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +module.exports = { + getSemesters, + getSemesterAnnouncements, + editSemester, + createSemester, +}; diff --git a/server/server/routing/functions/sponsors-func.js b/server/server/routing/functions/sponsors-func.js new file mode 100644 index 00000000..228ab007 --- /dev/null +++ b/server/server/routing/functions/sponsors-func.js @@ -0,0 +1,364 @@ +const DB_CONFIG = require("../database/db_config"); + +/** + * Create a new sponsor + */ +const createSponsor = (db, body, userId) => { + return new Promise((resolve, reject) => { + let createSponsorQuery = ` + INSERT into sponsors( + fname, + lname, + company, + division, + email, + phone, + association, + type + ) + values (?,?,?,?,?,?,?,?) + `; + + let createSponsorParams = [ + body.fname, + body.lname, + body.company, + body.division, + body.email, + body.phone, + body.association, + body.type, + ]; + + db.query(createSponsorQuery, createSponsorParams) + .then(() => { + resolve({ statusCode: 200, error: null }); + }) + .catch((err) => { + resolve({ statusCode: 500, error: err }); + }); + }); +}; + +/** + * Edit an existing sponsor + */ +const editSponsor = (db, body) => { + return new Promise((resolve, reject) => { + let updateSponsorQuery = ` + UPDATE sponsors + SET fname = ?, + lname = ?, + company = ?, + division = ?, + email = ?, + phone = ?, + association = ?, + type = ?, + inActive = ?, + doNotEmail = ? + WHERE sponsor_id = ? + `; + + let inActive = body.inActive === "true" || body.inActive === "1"; + let doNotEmail = body.doNotEmail === "true" || body.doNotEmail === "1"; + + let updateSponsorParams = [ + body.fname, + body.lname, + body.company, + body.division, + body.email, + body.phone, + body.association, + body.type, + inActive, + doNotEmail, + body.sponsor_id, + ]; + + db.query(updateSponsorQuery, updateSponsorParams) + .then(() => { + resolve({ statusCode: 200, error: null }); + }) + .catch((err) => { + resolve({ statusCode: 500, error: err }); + }); + }); +}; + +/** + * Create a sponsor note + */ +const createSponsorNote = ( + db, + noteContent, + sponsorId, + userId, + mockId, + previousNote, +) => { + return new Promise(async (resolve, reject) => { + let insertQuery = ` + INSERT into sponsor_notes + (note_content, sponsor, author, mock_id, previous_note) + values (?, ?, ?, ?, ?)`; + + let status = 500; + let error = null; + + try { + await db.query(insertQuery, [ + noteContent, + sponsorId, + userId, + mockId, + previousNote, + ]); + status = 200; + } catch (err) { + status = 500; + error = err; + } + + resolve([status, error]); + }); +}; + +/** + * Get all sponsors with pagination + */ +const getAllSponsors = (db, userType, resultLimit, offset) => { + return new Promise((resolve, reject) => { + if (userType === "student") { + resolve({ sponsors: [], sponsorsCount: 0 }); + return; + } + + let getSponsorsQuery = ` + SELECT * + FROM sponsors + ORDER BY + sponsors.company ASC, + sponsors.division ASC, + sponsors.fname ASC, + sponsors.lname ASC + LIMIT ? + OFFSET ? + `; + + let queryParams = [resultLimit || -1, offset || 0]; + let getSponsorsCount = `SELECT COUNT(*) FROM sponsors`; + + const sponsorsPromise = db.query(getSponsorsQuery, queryParams); + const countPromise = db.query(getSponsorsCount); + + Promise.all([countPromise, sponsorsPromise]) + .then(([[countResult], sponsors]) => { + resolve({ + sponsors, + sponsorsCount: countResult[Object.keys(countResult)[0]], + }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get sponsor for a project + */ +const getProjectSponsor = (db, projectId) => { + return new Promise((resolve, reject) => { + let query = `SELECT * FROM sponsors + WHERE sponsor_id = (SELECT sponsor FROM projects WHERE project_id = ?)`; + + db.query(query, [projectId]) + .then((sponsors) => { + resolve(sponsors); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all projects for a sponsor + */ +const getSponsorProjects = (db, sponsorId) => { + return new Promise((resolve, reject) => { + let query = ` + SELECT * + FROM projects + WHERE sponsor = ? + `; + + db.query(query, [sponsorId]) + .then((projects) => { + resolve(projects); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get notes for a sponsor + */ +const getSponsorNotes = (db, sponsorId) => { + return new Promise((resolve, reject) => { + let query = ` + SELECT sponsor_notes.*, + users.fname, users.lname, users.email, users.type, + (SELECT users.fname || ' ' || users.lname FROM users WHERE users.system_id = sponsor_notes.mock_id) AS mock_name + FROM sponsor_notes + JOIN users ON users.system_id = sponsor_notes.author + WHERE sponsor_notes.sponsor = ? + ORDER BY creation_date + `; + + db.query(query, [sponsorId]) + .then((notes) => { + resolve(notes); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Search for sponsors with pagination + */ +const searchForSponsor = (db, userType, searchQuery, resultLimit, offset) => { + return new Promise((resolve, reject) => { + if (userType === "student") { + resolve({ sponsors: [], sponsorsCount: 0 }); + return; + } + + const searchQueryParam = searchQuery || ""; + + let getSponsorsQuery = ` + SELECT * + FROM sponsors + WHERE sponsors.OID NOT IN ( + SELECT OID + FROM sponsors + WHERE + company LIKE ? + OR division LIKE ? + OR fname LIKE ? + OR lname LIKE ? + ORDER BY + company, + division, + fname, + lname + LIMIT ? + ) AND ( + sponsors.company LIKE ? + OR sponsors.division LIKE ? + OR sponsors.fname LIKE ? + OR sponsors.lname LIKE ? + ) + ORDER BY + sponsors.company, + sponsors.division, + sponsors.fname, + sponsors.lname + LIMIT ? + `; + + let getSponsorsCount = `SELECT COUNT(*) + FROM sponsors + WHERE + company LIKE ? + OR division LIKE ? + OR fname LIKE ? + OR lname LIKE ? + `; + + let queryParams = [ + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + offset || 0, + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + resultLimit || 0, + ]; + + let countParams = [ + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + "%" + searchQueryParam + "%", + ]; + + const sponsorsPromise = db.query(getSponsorsQuery, queryParams); + const countPromise = db.query(getSponsorsCount, countParams); + + Promise.all([countPromise, sponsorsPromise]) + .then(([[countResult], sponsors]) => { + resolve({ + sponsors, + sponsorsCount: countResult[Object.keys(countResult)[0]], + }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all sponsor info + */ +const selectAllSponsorInfo = (db) => { + return new Promise((resolve, reject) => { + db.selectAll(DB_CONFIG.tableNames.sponsor_info) + .then((sponsors) => { + resolve(sponsors); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get sponsor data for CSV export + */ +const getSponsorData = (db) => { + return new Promise((resolve, reject) => { + let query = `SELECT * FROM sponsors WHERE inActive = 0 AND doNotEmail = 0`; + + db.query(query, []) + .then((sponsors) => { + resolve(sponsors); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +module.exports = { + createSponsor, + editSponsor, + createSponsorNote, + getAllSponsors, + getProjectSponsor, + getSponsorProjects, + getSponsorNotes, + searchForSponsor, + selectAllSponsorInfo, + getSponsorData, +}; diff --git a/server/server/routing/functions/submissions-func.js b/server/server/routing/functions/submissions-func.js new file mode 100644 index 00000000..3938c667 --- /dev/null +++ b/server/server/routing/functions/submissions-func.js @@ -0,0 +1,130 @@ +const path = require("path"); +const { ROLES } = require("../consts"); + +const ACTION_TARGETS = { + ADMIN: "admin", + COACH: "coach", + TEAM: "team", + INDIVIDUAL: "individual", + COACH_ANNOUNCEMENT: "coach_announcement", + STUDENT_ANNOUNCEMENT: "student_announcement", + PEER_EVALUATION: "peer_evaluation", +}; + +/** + * Get submission form data and files + */ +const getSubmission = (db, user, logId) => { + return new Promise((resolve, reject) => { + let getSubmissionQuery = ""; + let params = []; + + switch (user.type) { + case ROLES.STUDENT: + getSubmissionQuery = `SELECT action_log.form_data, action_log.files + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + WHERE action_log.action_log_id = ? AND (actions.action_target = '${ACTION_TARGETS.TEAM}' OR action_log.system_id = ?)`; + params = [logId, user.system_id]; + break; + case ROLES.COACH: + getSubmissionQuery = `SELECT action_log.form_data, action_log.files + FROM action_log + JOIN project_coaches ON project_coaches.project_id = action_log.project + WHERE action_log.action_log_id = ? AND project_coaches.coach_id = ?`; + params = [logId, user.system_id]; + break; + case ROLES.ADMIN: + getSubmissionQuery = `SELECT action_log.form_data, action_log.files + FROM action_log + WHERE action_log.action_log_id = ?`; + params = [logId]; + break; + default: + reject(new Error("Unknown Role")); + return; + } + + db.query(getSubmissionQuery, params) + .then((submissions) => { + resolve(submissions); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get submission file path and validate access + */ +const getSubmissionFile = (db, user, logId, fileName) => { + return new Promise(async (resolve, reject) => { + let getSubmissionQuery = ""; + let params = []; + + switch (user.type) { + case ROLES.STUDENT: + getSubmissionQuery = `SELECT action_log.files, action_log.project, action_log.system_id, actions.action_id, actions.action_target + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + WHERE action_log.action_log_id = ? AND (actions.action_target = '${ACTION_TARGETS.TEAM}' OR action_log.system_id = ?)`; + params = [logId, user.system_id]; + break; + case ROLES.COACH: + getSubmissionQuery = `SELECT action_log.files, action_log.project, action_log.system_id, actions.action_id, actions.action_target + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + JOIN project_coaches ON project_coaches.project_id = action_log.project + WHERE action_log.action_log_id = ? AND project_coaches.coach_id = ?`; + params = [logId, user.system_id]; + break; + case ROLES.ADMIN: + getSubmissionQuery = `SELECT action_log.files, action_log.project, action_log.system_id, actions.action_id, actions.action_target + FROM action_log + JOIN actions ON actions.action_id = action_log.action_template + WHERE action_log.action_log_id = ?`; + params = [logId]; + break; + default: + reject(new Error("Unknown Role")); + return; + } + + try { + const result = await db.query(getSubmissionQuery, params); + const { files, project, action_target, system_id, action_id } = + result[0] || {}; + + let fileList = []; + if (files) { + fileList = files.split(","); + } + + if ( + fileList.includes(fileName) && + project && + action_target && + system_id && + action_id + ) { + const filePath = path.join( + __dirname, + `../project_docs/${project}/${action_target}/${action_id}/${system_id}/${fileName}`, + ); + resolve(filePath); + } else { + reject( + new Error("File not found or you are unauthorized to view file"), + ); + } + } catch (err) { + reject(err); + } + }); +}; + +module.exports = { + getSubmission, + getSubmissionFile, +}; diff --git a/server/server/routing/functions/time_logging-func.js b/server/server/routing/functions/time_logging-func.js new file mode 100644 index 00000000..d240abe9 --- /dev/null +++ b/server/server/routing/functions/time_logging-func.js @@ -0,0 +1,229 @@ +const moment = require("moment"); +const CONSTANTS = require("../consts"); +const { ROLES } = require("../consts"); + +/** + * Get average time logged per user for a project + */ +const avgTime = (db, projectId) => { + return new Promise((resolve, reject) => { + const sql = + "SELECT ROUND(AVG(CASE WHEN active != 0 THEN time_amount ELSE NULL END), 2) AS avgTime, system_id FROM time_log WHERE project = ? GROUP BY system_id"; + + db.query(sql, [projectId]) + .then((time) => { + resolve(time); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Create a new time log entry + */ +const createTimeLog = (db, user, body) => { + return new Promise((resolve, reject) => { + // Validate that the work date is not in the future + const workDate = new Date(body.date); + const currentDate = new Date(); + const currentDateOnly = new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + currentDate.getDate(), + ); + const workDateOnly = new Date( + workDate.getFullYear(), + workDate.getMonth(), + workDate.getDate(), + ); + + if (workDateOnly > currentDateOnly) { + reject(new Error("Cannot log time for future dates")); + return; + } + + // Validate that the work date is within the past 14 days + const twoWeeksAgo = new Date(currentDateOnly); + twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); + + if (workDateOnly < twoWeeksAgo) { + reject(new Error("Cannot log time for dates older than 14 days")); + return; + } + + let mock_id = user.mock ? user.mock.system_id : ""; + + const sql = `INSERT INTO time_log + (semester, system_id, project, mock_id, work_date, time_amount, work_comment) + VALUES (?,?,?,?,?,?,?)`; + + const params = [ + user.semester_group, + user.system_id, + user.project, + mock_id, + body.date, + body.time_amount, + body.comment, + ]; + + db.query(sql, params) + .then(() => { + resolve(); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Remove a time log entry + */ +const removeTime = (db, timeLogId) => { + return new Promise((resolve, reject) => { + if (!timeLogId) { + reject(new Error("No Id Provided")); + return; + } + + const sql = "UPDATE time_log SET active=0 WHERE time_log_id = ?"; + + db.query(sql, [timeLogId]) + .then(() => { + resolve(); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get time logs for a project based on user role + */ +const getTimeLogs = (db, user, projectId) => { + return new Promise((resolve, reject) => { + let getTimeLogQuery = ""; + let params = []; + + switch (user.type) { + case ROLES.STUDENT: + getTimeLogQuery = `SELECT time_log.time_log_id, time_log.submission_datetime, time_log.time_amount, time_log.system_id, time_log.mock_id, time_log.project, time_log.work_date, time_log.work_comment,time_log.active, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) mock_name + FROM time_log + WHERE time_log.project = ? + ORDER BY time_log.work_date DESC`; + params = [user.project]; + break; + case ROLES.COACH: + case ROLES.ADMIN: + getTimeLogQuery = `SELECT time_log.*, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) AS name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) AS mock_name + FROM time_log + WHERE time_log.project = ?`; + params = [projectId]; + break; + + default: + reject(new Error("Unknown Role")); + return; + } + + db.query(getTimeLogQuery, params) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all time logs with pagination + */ +const getAllTimeLogs = (db, user, resultLimit, offset) => { + return new Promise((resolve, reject) => { + let getTimeLogQuery = ""; + let queryParams = []; + let getTimeLogCount = ""; + let countParams = []; + + switch (user.type) { + case ROLES.STUDENT: + // NOTE: Technically, users are able to see if coaches submitted time logs to other projects, but they should not be able to see the actual submission content form this query so that should be fine + // This is because of the "OR users.type = '${ROLES.COACH}'" part of the following query. + getTimeLogQuery = `SELECT time_log.*, + projects.display_name, projects.title, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) mock_name + FROM time_log + JOIN projects ON projects.project_id = time_log.project + WHERE time_log.project = ? + ORDER BY time_log.work_date DESC`; + queryParams = [user.project]; + getTimeLogCount = `SELECT COUNT(*) FROM time_log + WHERE time_log.project = ? + AND time_log.system_id in (SELECT users.system_id FROM users WHERE users.project = ?)`; + countParams = [user.project, user.project]; + break; + case ROLES.COACH: + getTimeLogQuery = `SELECT time_log.*, + projects.display_name, projects.title, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) mock_name + FROM time_log + JOIN projects ON projects.project_id = time_log.project + WHERE time_log.project IN (SELECT project_id FROM project_coaches WHERE coach_id = ?) + ORDER BY time_log.work_date DESC`; + queryParams = [user.system_id]; + getTimeLogCount = `SELECT COUNT(*) FROM time_log WHERE time_log.project IN (SELECT project_id FROM project_coaches WHERE coach_id = ?)`; + countParams = [user.system_id]; + break; + case ROLES.ADMIN: + getTimeLogQuery = `SELECT time_log.*, + projects.display_name, projects.title, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.system_id) name, + (SELECT group_concat(users.fname || ' ' || users.lname) FROM users WHERE users.system_id = time_log.mock_id) mock_name + FROM time_log + JOIN projects ON projects.project_id = time_log.project + ORDER BY time_log.work_date DESC`; + queryParams = [resultLimit, offset * resultLimit]; + getTimeLogCount = `SELECT COUNT(*) FROM time_log`; + countParams = []; + break; + default: + reject(new Error("Unknown Role")); + return; + } + + queryParams.push(resultLimit, offset * resultLimit); + + const timeLogsPromise = db.query(getTimeLogQuery, queryParams); + const timeLogsCountPromise = db.query(getTimeLogCount, countParams); + + Promise.all([timeLogsCountPromise, timeLogsPromise]) + .then(([[timeLogCount], timeLogs]) => { + resolve({ + timeLogCount: timeLogCount[Object.keys(timeLogCount)[0]], + timeLogs: timeLogs, + }); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +module.exports = { + avgTime, + createTimeLog, + removeTime, + getTimeLogs, + getAllTimeLogs, +}; diff --git a/server/server/routing/functions/users-func.js b/server/server/routing/functions/users-func.js new file mode 100644 index 00000000..e6bd9f4b --- /dev/null +++ b/server/server/routing/functions/users-func.js @@ -0,0 +1,322 @@ +const moment = require("moment"); +const DB_CONFIG = require("../database/db_config"); +const CONSTANTS = require("../consts"); +const { ROLES } = require("../consts"); + +/** + * Get all student information + */ +const selectAllStudentInfo = (db) => { + return new Promise((resolve, reject) => { + let getStudentsQuery = ` + SELECT * + FROM users + LEFT JOIN semester_group + ON users.semester_group = semester_group.semester_id + WHERE type = 'student' + ORDER BY semester_group desc + `; + db.query(getStudentsQuery) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all non-student information (coaches, admins) + */ +const selectAllNonStudentInfo = (db) => { + return new Promise((resolve, reject) => { + let getUsersQuery = ` + SELECT * + FROM users + LEFT JOIN semester_group + ON users.semester_group = semester_group.semester_id + WHERE type != 'student' + `; + db.query(getUsersQuery) + .then((values) => { + resolve(values); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get semester students based on user role + */ +const getSemesterStudents = (db, user, projectId) => { + return new Promise((resolve, reject) => { + let query = ""; + let params = []; + switch (user.type) { + case ROLES.STUDENT: + query = ` + SELECT users.* + FROM users + WHERE users.semester_group = ( + SELECT semester_group FROM users WHERE system_id = ? + ) AND users.type = 'student'`; + params = [user.system_id]; + break; + + case ROLES.COACH: + query = ` + SELECT users.* FROM users + LEFT JOIN semester_group + ON users.semester_group = semester_group.semester_id + WHERE users.semester_group IN ( + SELECT projects.semester FROM projects + WHERE projects.project_id IN ( + SELECT project_coaches.project_id FROM project_coaches + WHERE project_coaches.coach_id = ? + ) + )`; + params = [user.system_id]; + break; + + case ROLES.ADMIN: + query = `SELECT * FROM users + LEFT JOIN semester_group + ON users.semester_group = semester_group.semester_id + WHERE users.type = 'student'`; + break; + default: + break; + } + + db.query(query, params) + .then((users) => { + if (user.type === ROLES.STUDENT) { + users = users.map((u) => { + let output = {}; + if (u.project === user.project) { + output["last_login"] = u["last_login"]; + output["prev_login"] = u["prev_login"]; + } + output["active"] = u["active"]; + output["email"] = u["email"]; + output["fname"] = u["fname"]; + output["lname"] = u["lname"]; + output["project"] = u["project"]; + output["semester_group"] = u["semester_group"]; + output["system_id"] = u["system_id"]; + output["type"] = u["type"]; + return output; + }); + } + resolve(users); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get project members + */ +const getProjectMembers = (db, projectId) => { + return new Promise((resolve, reject) => { + let query = `SELECT users.*, project_coaches.project_id FROM users + LEFT JOIN project_coaches ON project_coaches.coach_id = users.system_id + WHERE users.project = ? OR project_coaches.project_id = ?`; + + let params = [projectId, projectId]; + + db.query(query, params) + .then((users) => { + resolve(users); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Get all active users + */ +const getActiveUsers = (db) => { + return new Promise((resolve, reject) => { + let query = `SELECT ${CONSTANTS.SIGN_IN_SELECT_ATTRIBUTES} + FROM users + WHERE active = ''`; + db.query(query) + .then((users) => { + resolve(users); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Create a new user + */ +const createUser = (db, body) => { + return new Promise((resolve, reject) => { + const active = + body.active === "false" ? moment().format(CONSTANTS.datetime_format) : ""; + + const viewOnly = body.viewOnly === "true" ? "TRUE" : "FALSE"; + + // Default profile_info with required fields + const defaultProfileInfo = JSON.stringify({ + additional_info: "", + dark_mode: false, + gantt_view: true, + }); + + const sql = `INSERT INTO ${DB_CONFIG.tableNames.users} + (system_id, fname, lname, email, type, semester_group, project, active, view_only, profile_info) + VALUES (?,?,?,?,?,?,?,?,?,?)`; + + const params = [ + body.system_id, + body.fname, + body.lname, + body.email, + body.type, + body.semester_group === "" ? null : body.semester_group, + body.project === "" ? null : body.project, + active, + viewOnly, + defaultProfileInfo, + ]; + + db.query(sql, params) + .then(() => { + resolve(); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +/** + * Batch create multiple users + */ +const batchCreateUser = (db, users) => { + return new Promise(async (resolve, reject) => { + const failedUsers = []; + const successUsers = []; + + for (const user of users) { + // Default profile_info with required fields + const defaultProfileInfo = JSON.stringify({ + additional_info: "", + dark_mode: false, + gantt_view: true, + }); + + const values = [ + user.system_id, + user.fname, + user.lname, + user.email, + user.type, + user.semester_group === "" ? null : user.semester_group, + user.active.toLocaleLowerCase() === "false" + ? moment().format(CONSTANTS.datetime_format) + : "", + defaultProfileInfo, + ]; + + try { + await db.query( + `INSERT INTO ${DB_CONFIG.tableNames.users} + (system_id, fname, lname, email, type, semester_group, active, profile_info) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + values, + ); + successUsers.push(user); + } catch (err) { + let errorMessage = err.message; + + // Provide more user-friendly error messages for common constraint violations + if (err.code === "SQLITE_CONSTRAINT") { + if ( + err.message.includes("UNIQUE constraint failed: users.system_id") + ) { + errorMessage = `System ID '${user.system_id}' already exists`; + } else if ( + err.message.includes("UNIQUE constraint failed: users.email") + ) { + errorMessage = `Email '${user.email}' already exists`; + } else { + errorMessage = "Duplicate data - user may already exist"; + } + } + + failedUsers.push({ user, error: errorMessage }); + } + } + + resolve({ successUsers, failedUsers }); + }); +}; + +/** + * Edit user information + */ +const editUser = (db, body) => { + return new Promise((resolve, reject) => { + const active = + body.active === "false" ? moment().format(CONSTANTS.datetime_format) : ""; + + const viewOnly = body.viewOnly === "true" ? "TRUE" : "FALSE"; + + let updateQuery = ` + UPDATE users + SET fname = ?, + lname = ?, + email = ?, + type = ?, + semester_group = ?, + project = ?, + active = ?, + view_only = ? + WHERE system_id = ? + `; + + let params = [ + body.fname, + body.lname, + body.email, + body.type, + body.semester_group || null, + body.project || null, + active, + viewOnly, + body.system_id, + ]; + + db.query(updateQuery, params) + .then(() => { + resolve(); + }) + .catch((err) => { + reject(err); + }); + }); +}; + +module.exports = { + selectAllStudentInfo, + selectAllNonStudentInfo, + getSemesterStudents, + getProjectMembers, + getActiveUsers, + createUser, + batchCreateUser, + editUser, +}; diff --git a/server/server/routing/routes/actions_routes.js b/server/server/routing/routes/actions_routes.js new file mode 100644 index 00000000..8a51f2d3 --- /dev/null +++ b/server/server/routing/routes/actions_routes.js @@ -0,0 +1,176 @@ +const express = require("express"); +const router = express.Router(); +const UserAuth = require("../user_auth"); +const { body, validationResult } = require("express-validator"); +const { + getActions, + getTimelineActions, + getLateSubmission, + getActionLogs, + getAllActionLogs, + submitAction, + getCoachFeedback, + createAction, + editAction, +} = require("../functions/actions-func"); + +module.exports = (db) => { + // Get all actions (admin only) + router.get("/getActions", [UserAuth.isAdmin], (req, res, next) => { + getActions(db) + .then((values) => { + res.send(values); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get timeline actions for a project + router.get( + "/getTimelineActions", + [UserAuth.isSignedIn], + async (req, res, next) => { + getTimelineActions(db, req.query.project_id, req.user) + .then((values) => { + res.send(values); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Get late submission info for an action log + router.get("/getLateSubmission", [UserAuth.isSignedIn], (req, res, next) => { + getLateSubmission(db, req.query.log_id) + .then((values) => { + res.send(values); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get action logs for a specific action + router.get("/getActionLogs", [UserAuth.isSignedIn], (req, res, next) => { + getActionLogs(db, req.user, req.query.action_id, req.query.project_id) + .then((values) => { + res.send(values); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get all action logs with pagination + router.get( + "/getAllActionLogs", + [UserAuth.isSignedIn], + async (req, res, next) => { + const { resultLimit, offset } = req.query; + + getAllActionLogs(db, req.user, resultLimit, offset) + .then(({ actionLogCount, actionLogs }) => { + res.send({ + actionLogCount: actionLogCount, + actionLogs: actionLogs, + }); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Submit an action with file attachments + router.post( + "/submitAction", + [UserAuth.isSignedIn, UserAuth.canWrite, body("*").trim()], + async (req, res, next) => { + let result = validationResult(req); + + if (result.errors.length !== 0) { + return res.status(400).send("Input is invalid"); + } + + submitAction(db, req.user, req.body, req.files, __dirname) + .then(() => { + return res.sendStatus(200); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = err.statusCode || 500; + return next(error); + }); + }, + ); + + // Get coach feedback for a project + router.get( + "/getCoachFeedback", + [UserAuth.isSignedIn], + async (req, res, next) => { + getCoachFeedback(db, req.query.project_id) + .then((feedback) => { + res.send(feedback); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Create a new action template + router.post( + "/createAction", + [UserAuth.isAdmin, UserAuth.canWrite, body("page_html").unescape()], + (req, res, next) => { + createAction(db, req.body) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Edit an existing action template + router.post( + "/editAction", + [UserAuth.isAdmin, UserAuth.canWrite, body("page_html").unescape()], + (req, res, next) => { + editAction(db, req.body) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/archives_routes.js b/server/server/routing/routes/archives_routes.js new file mode 100644 index 00000000..61d2573e --- /dev/null +++ b/server/server/routing/routes/archives_routes.js @@ -0,0 +1,252 @@ +const express = require("express"); +const router = express.Router(); +const path = require("path"); + +const { body } = require("express-validator"); +const { + createArchive, + getActiveArchiveProjects, + getArchiveProjects, + getArchiveProject, + getArchiveFromSlug, + getArchiveFromProject, + searchForArchive, + editArchive, + editArchiveStudent, + createArchiveStudent, + getArchive, + getArchivePoster, + getArchiveVideo, + getArchiveImage, +} = require("../functions/archives-func"); + +module.exports = (db) => { + const UserAuth = require("../user_auth"); + + // Create archive (admin only) + router.post( + "/createArchive", + [ + UserAuth.isAdmin, + UserAuth.canWrite, + body("featured") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty"), + ], + (req, res, next) => { + createArchive(db, req.body, req.user) + .then((response) => { + return res.status(200).send(response); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Get all archives (admin only) + router.get("/getArchive", [UserAuth.isAdmin], (req, res, next) => { + getArchive(db) + .then((values) => { + res.send(values); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get active archive projects for display (featured or all) + router.get("/getActiveArchiveProjects", (req, res, next) => { + const { resultLimit, page, featured } = req.query; + getActiveArchiveProjects(db, resultLimit, page, featured) + .then((response) => { + res.send(response); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get all archive projects for admin view + router.get("/getArchiveProjects", (req, res, next) => { + const { resultLimit, offset } = req.query; + getArchiveProjects(db, resultLimit, offset) + .then((response) => { + res.send(response); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get specific archive project by ID (admin only) + router.get("/getArchiveProject", [UserAuth.isAdmin], (req, res, next) => { + getArchiveProject(db, req.query.archive_id) + .then((project) => { + res.send(project); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Edit archive (admin only) + router.post( + "/editArchive", + [UserAuth.isAdmin, UserAuth.canWrite], + async (req, res, next) => { + editArchive(db, req.body, req.user) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Edit archive as student with file uploads + router.post( + "/editArchiveStudent", + [UserAuth.isSignedIn, UserAuth.canWrite], + async (req, res, next) => { + const files = req.files; + editArchiveStudent(db, req.body, req.user, files) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Create archive as student with optional file uploads + router.post( + "/createArchiveStudent", + [ + UserAuth.isSignedIn, + UserAuth.canWrite, + body("featured") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty"), + ], + async (req, res, next) => { + const files = req.files; + createArchiveStudent(db, req.body, req.user, files) + .then((response) => { + return res.status(200).send(response); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Get archive poster file + router.get("/getArchivePoster", (req, res, next) => { + try { + const filePath = getArchivePoster(req.query.fileName); + res.sendFile(filePath); + } catch (err) { + const error = new Error(err); + error.statusCode = 500; + return next(error); + } + }); + + // Get archive video file + router.get("/getArchiveVideo", (req, res, next) => { + try { + const filePath = getArchiveVideo(req.query.fileName); + res.sendFile(filePath); + } catch (err) { + const error = new Error(err); + error.statusCode = 500; + return next(error); + } + }); + + // Get archive image file + router.get("/getArchiveImage", (req, res, next) => { + try { + const filePath = getArchiveImage(req.query.fileName); + res.sendFile(filePath); + } catch (err) { + const error = new Error(err); + error.statusCode = 500; + return next(error); + } + }); + + // Search archive projects + router.get("/searchForArchive", (req, res, next) => { + const { resultLimit, offset, searchQuery, inactive } = req.query; + searchForArchive(db, resultLimit, offset, searchQuery, inactive) + .then((response) => { + res.send(response); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get archive by URL slug + router.get("/getArchiveFromSlug", (req, res, next) => { + getArchiveFromSlug(db, req.query.url_slug) + .then((values) => { + res.status(200).send(values); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get archives for a project + router.get("/getArchiveFromProject", (req, res, next) => { + getArchiveFromProject(db, req.query.project_id) + .then((values) => { + res.status(200).send(values); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + return router; +}; diff --git a/server/server/routing/routes/dashboard_routes.js b/server/server/routing/routes/dashboard_routes.js new file mode 100644 index 00000000..0e3dab35 --- /dev/null +++ b/server/server/routing/routes/dashboard_routes.js @@ -0,0 +1,238 @@ +const express = require("express"); +const router = express.Router(); +const UserAuth = require("../user_auth"); +const { + getAdditionalInfo, + editAdditionalInfo, + setDarkMode, + getDarkMode, + getPeerEvals, + setGanttView, + getGanttView, + setCalendarView, + getCalendarView, + setMilestoneView, + getMilestoneView, +} = require("../functions/dashboard-func"); + +module.exports = (db) => { + // Get additional user info + router.get( + "/getAdditionalInfo", + [UserAuth.isSignedIn], + async (req, res, next) => { + const requestedUserId = req.query.system_id; + + if (!requestedUserId) { + return res.status(400).send({ error: "User ID is required" }); + } + + getAdditionalInfo(db, requestedUserId) + .then((result) => { + res.send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = err.message.includes("not found") ? 404 : 500; + next(error); + }); + }, + ); + + // Edit additional user info + router.post( + "/editAdditionalInfo", + [UserAuth.isSignedIn], + async (req, res, next) => { + const { system_id, additional_info } = req.body; + + if (!system_id || additional_info === undefined) { + return res.status(400).send({ + error: "system_id and additional_info are required", + }); + } + + editAdditionalInfo(db, system_id, additional_info) + .then((result) => { + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = 500; + next(error); + }); + }, + ); + + // Set dark mode preference + router.post("/setDarkMode", [UserAuth.isSignedIn], async (req, res, next) => { + const { system_id, dark_mode } = req.body; + + setDarkMode(db, system_id, dark_mode) + .then((result) => { + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = 500; + next(error); + }); + }); + + // Get dark mode preference + router.get("/getDarkMode", [UserAuth.isSignedIn], async (req, res, next) => { + const { system_id } = req.query; + + getDarkMode(db, system_id) + .then((result) => { + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = err.message.includes("not found") ? 404 : 500; + next(error); + }); + }); + + // Get peer evaluations + router.get("/getPeerEvals", [UserAuth.isCoachOrAdmin], (req, res, next) => { + const semesterNumber = req.query.semester; + + getPeerEvals(db, semesterNumber) + .then((logs) => { + res.send(logs); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = 500; + next(error); + }); + }); + + // Set gantt view preference + router.post( + "/setGanttView", + [UserAuth.isSignedIn], + async (req, res, next) => { + const { system_id, gantt_view } = req.body; + + setGanttView(db, system_id, gantt_view) + .then((result) => { + console.log( + "Gantt view preference updated successfully", + gantt_view, + system_id, + ); + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = 500; + next(error); + }); + }, + ); + + // Get gantt view preference + router.get("/getGanttView", [UserAuth.isSignedIn], async (req, res, next) => { + const { system_id } = req.query; + + getGanttView(db, system_id) + .then((result) => { + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = err.message.includes("not found") ? 404 : 500; + next(error); + }); + }); + + // Set calendar view preference + router.post( + "/setCalendarView", + [UserAuth.isSignedIn], + async (req, res, next) => { + const { system_id, calendar_view } = req.body; + + setCalendarView(db, system_id, calendar_view) + .then((result) => { + console.log( + "Calendar view preference updated successfully", + calendar_view, + system_id, + ); + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = 500; + next(error); + }); + }, + ); + + // Get calendar view preference + router.get( + "/getCalendarView", + [UserAuth.isSignedIn], + async (req, res, next) => { + const { system_id } = req.query; + + getCalendarView(db, system_id) + .then((result) => { + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = err.message.includes("not found") ? 404 : 500; + next(error); + }); + }, + ); + + // Set milestone view preference + router.post( + "/setMilestoneView", + [UserAuth.isSignedIn], + async (req, res, next) => { + const { system_id, milestone_view } = req.body; + + setMilestoneView(db, system_id, milestone_view) + .then((result) => { + console.log( + "Milestone view preference updated successfully", + milestone_view, + system_id, + ); + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = 500; + next(error); + }); + }, + ); + + // Get milestone view preference + router.get( + "/getMilestoneView", + [UserAuth.isSignedIn], + async (req, res, next) => { + const { system_id } = req.query; + + getMilestoneView(db, system_id) + .then((result) => { + res.status(200).send(result); + }) + .catch((err) => { + const error = new Error(err.message); + error.statusCode = err.message.includes("not found") ? 404 : 500; + next(error); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/dev_only_routes.js b/server/server/routing/routes/dev_only_routes.js new file mode 100644 index 00000000..2bebfe71 --- /dev/null +++ b/server/server/routing/routes/dev_only_routes.js @@ -0,0 +1,36 @@ +const express = require("express"); +const router = express.Router(); +const { + devOnlyGetAllUsersForLogin, + devOnlyRedeployDatabase, +} = require("../functions/dev-only-func"); + +module.exports = (db) => { + // Development only: Get all users for login + router.get("/DevOnlyGetAllUsersForLogin", (req, res) => { + devOnlyGetAllUsersForLogin(db) + .then((users) => { + res.send(users); + }) + .catch((err) => { + res.status(500).json({ + success: false, + message: "Failed to fetch users", + error: err.message, + }); + }); + }); + + // Development only: Redeploy database + router.put("/DevOnlyRedeployDatabase", async (req, res) => { + devOnlyRedeployDatabase() + .then((result) => { + res.status(200).json(result); + }) + .catch((error) => { + res.status(500).json(error); + }); + }); + + return router; +}; diff --git a/server/server/routing/routes/error_logs_routes.js b/server/server/routing/routes/error_logs_routes.js new file mode 100644 index 00000000..7eab9573 --- /dev/null +++ b/server/server/routing/routes/error_logs_routes.js @@ -0,0 +1,6 @@ +const express = require("express"); +const router = express.Router(); + +module.exports = (db) => { + return router; +}; diff --git a/server/server/routing/routes/files_routes.js b/server/server/routing/routes/files_routes.js new file mode 100644 index 00000000..6e10e04d --- /dev/null +++ b/server/server/routing/routes/files_routes.js @@ -0,0 +1,157 @@ +const express = require("express"); +const router = express.Router(); + +const { + uploadFiles, + uploadFilesStudent, + createDirectory, + renameDirectoryOrFile, + getFiles, + getProjectFiles, + removeFile, + removeDirectory, +} = require("../functions/files-func"); + +module.exports = (db) => { + const UserAuth = require("../user_auth"); + + // Upload files (admin only) + router.post( + "/uploadFiles", + [UserAuth.isAdmin, UserAuth.canWrite], + (req, res, next) => { + uploadFiles(req.files, req.body.path) + .then(({ msg, filesUploaded }) => { + res.send({ msg: msg, filesUploaded: filesUploaded }); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Upload files for student + router.post("/uploadFilesStudent", UserAuth.canWrite, (req, res, next) => { + uploadFilesStudent( + db, + req.files, + req.body.path, + req.body.archive, + req.body.column, + ) + .then(({ msg, filesUploaded }) => { + res.send({ msg: msg, filesUploaded: filesUploaded }); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Create a directory + router.post( + "/createDirectory", + [UserAuth.isAdmin, UserAuth.canWrite], + (req, res, next) => { + createDirectory(req.query.path) + .then(({ msg }) => { + res.send({ msg: msg }); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Rename directory or file + router.post( + "/renameDirectoryOrFile", + [UserAuth.isAdmin, UserAuth.canWrite], + (req, res, next) => { + renameDirectoryOrFile(req.query.oldPath, req.query.newPath) + .then(({ msg }) => { + res.send({ msg: msg }); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Get files in a directory + router.get("/getFiles", UserAuth.isAdmin, (req, res, next) => { + getFiles(req.query.path) + .then((fileData) => { + res.send(fileData); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get project files + router.get("/getProjectFiles", (req, res, next) => { + getProjectFiles() + .then((fileData) => { + res.send(fileData); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Remove a file + router.delete( + "/removeFile", + [UserAuth.isAdmin, UserAuth.canWrite], + (req, res, next) => { + removeFile(req.query.path) + .then(({ msg }) => { + res.send({ msg: msg }); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Remove a directory + router.delete( + "/removeDirectory", + [UserAuth.isAdmin, UserAuth.canWrite], + (req, res, next) => { + removeDirectory(req.query.path) + .then(({ msg }) => { + res.send({ msg: msg }); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/projects_routes.js b/server/server/routing/routes/projects_routes.js new file mode 100644 index 00000000..ab3e07d8 --- /dev/null +++ b/server/server/routing/routes/projects_routes.js @@ -0,0 +1,225 @@ +const express = require("express"); +const router = express.Router(); +const { + getActiveProjects, + getActiveCoaches, + getProjectCoaches, + getProjectStudents, + getProjectStudentNames, + selectAllCoachInfo, + getProjects, + getCandidateProjects, + getMyProjects, + getSemesterProjects, + getProjectDates, + editProject, + updateProposalStatus, +} = require("../functions/projects-func"); + +module.exports = (db) => { + router.get( + "/getActiveProjects", + [require("../user_auth").isSignedIn], + (req, res, next) => { + getActiveProjects(db) + .then((values) => { + res.send(values); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/getActiveCoaches", + [require("../user_auth").isCoachOrAdmin], + (req, res, next) => { + getActiveCoaches(db) + .then((coaches) => { + res.send(coaches); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/getProjectCoaches", + [require("../user_auth").isCoachOrAdmin], + (req, res, next) => { + getProjectCoaches(db, req.query.project_id) + .then((coaches) => { + res.send(coaches); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/getProjectStudents", + [require("../user_auth").isCoachOrAdmin], + (req, res, next) => { + getProjectStudents(db, req.query.project_id) + .then((students) => { + res.send(students); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/getProjectStudentNames", + [require("../user_auth").isSignedIn], + (req, res, next) => { + getProjectStudentNames(db, req.query.project_id, req.user.system_id) + .then((students) => { + res.send(students); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/selectAllCoachInfo", + [require("../user_auth").isCoachOrAdmin], + (req, res, next) => { + selectAllCoachInfo(db) + .then((coaches) => { + res.send(coaches); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/getProjects", + [require("../user_auth").isCoachOrAdmin], + (req, res, next) => { + getProjects(db) + .then((projects) => res.send(projects)) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/getCandidateProjects", + [require("../user_auth").isSignedIn], + (req, res, next) => { + getCandidateProjects(db) + .then((projects) => res.send(projects)) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/getMyProjects", + [require("../user_auth").isSignedIn], + (req, res, next) => { + getMyProjects(db, req.user) + .then((proposals) => res.send(proposals)) + .catch((err) => res.status(500).send(err)); + }, + ); + + router.get( + "/getSemesterProjects", + [require("../user_auth").isSignedIn], + (req, res, next) => { + getSemesterProjects(db, req.user) + .then((projects) => res.send(projects)) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/getProjectDates", + require("../user_auth").isSignedIn, + (req, res, next) => { + getProjectDates(db, req.query.semester) + .then((dates) => { + res.send(dates); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.post( + "/editProject", + [require("../user_auth").isAdmin, require("../user_auth").canWrite, ...[]], + (req, res, next) => { + editProject(db, req.body) + .then(() => { + return res.sendStatus(200); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.patch( + "/updateProposalStatus", + [require("../user_auth").isAdmin, require("../user_auth").canWrite, ...[]], + (req, res, next) => { + updateProposalStatus(db, req.body.status, req.body.project_id) + .then(() => { + res.sendStatus(200); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/proposals_routes.js b/server/server/routing/routes/proposals_routes.js new file mode 100644 index 00000000..251c8965 --- /dev/null +++ b/server/server/routing/routes/proposals_routes.js @@ -0,0 +1,212 @@ +const express = require("express"); +const router = express.Router(); +const { body, validationResult } = require("express-validator"); + +const { + getProposalPdfNames, + getProposalPdf, + getProposalAttachmentNames, + getProposalAttachment, + submitProposal, +} = require("../functions/proposals-func"); + +module.exports = (db) => { + const UserAuth = require("../user_auth"); + + // Get list of proposal PDF names + router.get("/getProposalPdfNames", UserAuth.isSignedIn, (req, res, next) => { + getProposalPdfNames() + .then((fileLinks) => { + res.send(fileLinks); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get proposal PDF + router.get("/getProposalPdf", UserAuth.isSignedIn, (req, res, next) => { + getProposalPdf(req.query.project_id) + .then((filePath) => { + res.sendFile(filePath); + }) + .catch((err) => { + console.error(err); + res.send("File not found"); + }); + }); + + // Get list of proposal attachment names + router.get( + "/getProposalAttachmentNames", + UserAuth.isSignedIn, + (req, res, next) => { + getProposalAttachmentNames(req.query.project_id) + .then((fileLinks) => { + res.send(fileLinks); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Get proposal attachment + router.get( + "/getProposalAttachment", + UserAuth.isSignedIn, + (req, res, next) => { + getProposalAttachment(req.query.project_id, req.query.name) + .then((filePath) => { + res.sendFile(filePath); + }) + .catch((err) => { + console.error(err); + res.send("File not found"); + }); + }, + ); + + // Submit a new proposal + router.post( + "/submitProposal", + [ + UserAuth.isSignedIn, + UserAuth.canWrite, + body("title") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }) + .withMessage("Title must be under 50 characters"), + body("organization") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("primary_contact") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("contact_email") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("contact_phone") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("background_info") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("project_description") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("project_scope") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("project_challenges") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("sponsor_provided_resources") + .trim() + .escape() + .isLength({ max: 5000 }), + body("constraints_assumptions") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("sponsor_deliverables") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + body("proprietary_info").trim().escape().isLength({ max: 5000 }), + body("sponsor_alternate_time").trim().escape().isLength({ max: 5000 }), + body("sponsor_avail_checked") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty"), + body("project_agreements_checked") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty"), + body("assignment_of_rights") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 5000 }), + ], + async (req, res, next) => { + let result = validationResult(req); + + if (result.errors.length !== 0) { + const errorMessages = result.errors + .map((error) => `${error.param}: ${error.msg}`) + .join(", "); + const error = new Error(`Validation failed: ${errorMessages}`); + error.statusCode = 400; + return next(error); + } + + submitProposal(db, req.body, req.files) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/semester_routes.js b/server/server/routing/routes/semester_routes.js new file mode 100644 index 00000000..a48e53a1 --- /dev/null +++ b/server/server/routing/routes/semester_routes.js @@ -0,0 +1,120 @@ +const express = require("express"); +const router = express.Router(); +const UserAuth = require("../user_auth"); +const { body, validationResult } = require("express-validator"); +const { + getSemesters, + getSemesterAnnouncements, + editSemester, + createSemester, +} = require("../functions/semester-func"); + +module.exports = (db) => { + // Get all semesters + router.get("/getSemesters", [UserAuth.isSignedIn], (req, res, next) => { + getSemesters(db) + .then((values) => { + res.send(values); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get semester announcements + router.get( + "/getSemesterAnnouncements", + [UserAuth.isSignedIn], + (req, res, next) => { + getSemesterAnnouncements(db, req.user, req.query.semester) + .then((values) => { + res.send(values); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = err.statusCode || 500; + return next(error); + }); + }, + ); + + // Edit an existing semester + router.post( + "/editSemester", + [UserAuth.isAdmin, UserAuth.canWrite, body("*").trim()], + (req, res, next) => { + editSemester(db, req.body) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Create a new semester + router.post( + "/createSemester", + [ + UserAuth.isAdmin, + UserAuth.canWrite, + body("name") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + body("dept") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + body("start_date") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + body("end_date") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + ], + (req, res, next) => { + let result = validationResult(req); + + if (result.errors.length !== 0) { + const errorMessages = result.errors + .map((error) => `${error.param}: ${error.msg}`) + .join(", "); + const error = new Error(`Error Creating Semester: ${errorMessages}`); + error.statusCode = 400; + return next(error); + } + + createSemester(db, req.body) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/sponsors_routes.js b/server/server/routing/routes/sponsors_routes.js new file mode 100644 index 00000000..1657b966 --- /dev/null +++ b/server/server/routing/routes/sponsors_routes.js @@ -0,0 +1,210 @@ +const express = require("express"); +const router = express.Router(); +const UserAuth = require("../user_auth"); +const { body, validationResult } = require("express-validator"); +const { + createSponsor, + editSponsor, + createSponsorNote, + getAllSponsors, + getProjectSponsor, + getSponsorProjects, + getSponsorNotes, + searchForSponsor, + selectAllSponsorInfo, + getSponsorData, +} = require("../functions/sponsors-func"); + +module.exports = (db) => { + router.get("/selectAllSponsorInfo", [UserAuth.isCoachOrAdmin], (req, res) => { + selectAllSponsorInfo(db) + .then((value) => { + res.send(value); + }) + .catch((err) => { + res.status(500).send(err); + }); + }); + + router.get("/getSponsorData", UserAuth.isAdmin, (req, res, next) => { + getSponsorData(db) + .then((response) => { + res.send(response); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + router.post( + "/createSponsor", + [UserAuth.isCoachOrAdmin, UserAuth.canWrite, body("page_html").unescape()], + (req, res, next) => { + let body = req.body; + + createSponsor(db, body) + .then(() => { + let note_content = "Sponsor created by " + req.user.system_id; + return createSponsorNote(db, { + note_content, + sponsor_id: body.sponsor_id, + author: req.user.system_id, + mock_id: req.user.mock ? req.user.mock.system_id : null, + previous_note: null, + }); + }) + .then(() => { + res.status(200).send(); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.post( + "/editSponsor", + [UserAuth.isCoachOrAdmin, UserAuth.canWrite, body("page_html").unescape()], + (req, res, next) => { + let body = req.body; + + editSponsor(db, body) + .then(() => { + let changedFieldsMessageFirstPart = []; + let changedFieldsMessageSecondPart = []; + let changedFieldsMessageThirdPart = []; + + body.changed_fields = JSON.parse(body.changed_fields); + + for (const field of Object.keys(body.changed_fields)) { + changedFieldsMessageFirstPart.push(field); + changedFieldsMessageSecondPart.push(body.changed_fields[field][0]); + changedFieldsMessageThirdPart.push(body.changed_fields[field][1]); + } + + let note_content = + "Fields: " + + changedFieldsMessageFirstPart.join(", ") + + " were changed from: " + + changedFieldsMessageSecondPart.join(", ") + + " to: " + + changedFieldsMessageThirdPart.join(", "); + + return createSponsorNote(db, { + note_content, + sponsor_id: body.sponsor_id, + author: req.user.system_id, + mock_id: req.user.mock ? req.user.mock.system_id : null, + previous_note: null, + }); + }) + .then(() => { + res.status(200).send(); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.post( + "/createSponsorNote", + [UserAuth.isCoachOrAdmin, UserAuth.canWrite, body("page_html").unescape()], + (req, res, next) => { + let body = req.body; + let mock_id = req.user.mock ? req.user.mock.system_id : null; + + createSponsorNote(db, { + note_content: body.note_content, + sponsor_id: body.sponsor_id, + author: req.user.system_id, + mock_id: mock_id, + previous_note: body.previous_note, + }) + .then(() => { + res.status(200).send(); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get("/getAllSponsors", [UserAuth.isSignedIn], (req, res, next) => { + const { resultLimit, offset } = req.query; + + getAllSponsors(db, req.user.type, resultLimit, offset) + .then((result) => { + res.send(result); + }) + .catch((err) => { + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + router.get("/getProjectSponsor", [UserAuth.isSignedIn], (req, res) => { + getProjectSponsor(db, req.query.project_id) + .then((users) => { + res.send(users); + }) + .catch((err) => { + res.status(500).send(err); + }); + }); + + router.get("/getSponsorProjects", [UserAuth.isCoachOrAdmin], (req, res) => { + getSponsorProjects(db, req.query.sponsor_id) + .then((projects) => { + res.send(projects); + }) + .catch((err) => { + res.status(500).send(err); + }); + }); + + router.get( + "/getSponsorNotes", + [UserAuth.isCoachOrAdmin], + (req, res, next) => { + getSponsorNotes(db, req.query.sponsor_id) + .then((sponsorNotes) => { + res.send(sponsorNotes); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + router.get( + "/searchForSponsor", + [UserAuth.isCoachOrAdmin, body("page_html").escape()], + (req, res, next) => { + const { resultLimit, offset, searchQuery } = req.query; + + searchForSponsor(db, req.user.type, resultLimit, offset, searchQuery) + .then((result) => { + res.send(result); + }) + .catch((err) => { + res.status(500).send(err); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/submissions_routes.js b/server/server/routing/routes/submissions_routes.js new file mode 100644 index 00000000..4c8bbc62 --- /dev/null +++ b/server/server/routing/routes/submissions_routes.js @@ -0,0 +1,45 @@ +const express = require("express"); +const router = express.Router(); + +const { + getSubmission, + getSubmissionFile, +} = require("../functions/submissions-func"); + +module.exports = (db) => { + const UserAuth = require("../user_auth"); + + // Get submission form data and files + router.get("/getSubmission", [UserAuth.isSignedIn], (req, res, next) => { + getSubmission(db, req.user, req.query.log_id) + .then((submissions) => { + res.send(submissions); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get submission file + router.get( + "/getSubmissionFile", + [UserAuth.isSignedIn], + async (req, res, next) => { + getSubmissionFile(db, req.user, req.query.log_id, req.query.file) + .then((filePath) => { + return res.sendFile(filePath); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 404; + return next(error); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/time_logging_routes.js b/server/server/routing/routes/time_logging_routes.js new file mode 100644 index 00000000..ff04b91c --- /dev/null +++ b/server/server/routing/routes/time_logging_routes.js @@ -0,0 +1,100 @@ +const express = require("express"); +const router = express.Router(); + +const { + avgTime, + createTimeLog, + removeTime, + getTimeLogs, + getAllTimeLogs, +} = require("../functions/time_logging-func"); + +module.exports = (db) => { + const UserAuth = require("../user_auth"); + + // Get average time per user for a project + router.get("/avgTime", [UserAuth.isSignedIn], async (req, res, next) => { + avgTime(db, req.query.project_id) + .then((time) => { + console.log(time); + res.send(time); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Create a new time log + router.post("/createTimeLog", [UserAuth.canWrite], async (req, res, next) => { + createTimeLog(db, req.user, req.body) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + console.error(err); + let error = new Error(err); + error.statusCode = 400; + return next(error); + }); + }); + + // Remove a time log + router.post( + "/removeTime", + [UserAuth.isSignedIn, UserAuth.canWrite], + (req, res, next) => { + removeTime(db, req.body.id) + .then(() => { + res.status(200).send(); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 400; + return next(error); + }); + }, + ); + + // Get time logs for a project + router.get("/getTimeLogs", [UserAuth.isSignedIn], async (req, res, next) => { + getTimeLogs(db, req.user, req.query.project_id) + .then((values) => { + res.send(values); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get all time logs with pagination + router.get( + "/getAllTimeLogs", + [UserAuth.isSignedIn], + async (req, res, next) => { + const { resultLimit, offset } = req.query; + + getAllTimeLogs(db, req.user, resultLimit, offset) + .then(({ timeLogCount, timeLogs }) => { + res.send({ + timeLogCount: timeLogCount, + timeLogs: timeLogs, + }); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + return router; +}; diff --git a/server/server/routing/routes/users_routes.js b/server/server/routing/routes/users_routes.js new file mode 100644 index 00000000..8ab69ac5 --- /dev/null +++ b/server/server/routing/routes/users_routes.js @@ -0,0 +1,215 @@ +const express = require("express"); +const router = express.Router(); +const { body, validationResult } = require("express-validator"); + +const { + selectAllStudentInfo, + selectAllNonStudentInfo, + getSemesterStudents, + getProjectMembers, + getActiveUsers, + createUser, + batchCreateUser, + editUser, +} = require("../functions/users-func"); + +module.exports = (db) => { + const UserAuth = require("../user_auth"); + + // Get all student information + router.get( + "/selectAllStudentInfo", + [UserAuth.isCoachOrAdmin], + (req, res, next) => { + selectAllStudentInfo(db) + .then((values) => { + return res.send(values); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Get all non-student information + router.get( + "/selectAllNonStudentInfo", + [UserAuth.isAdmin], + (req, res, next) => { + selectAllNonStudentInfo(db) + .then((values) => { + return res.send(values); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Get semester students + router.get( + "/getSemesterStudents", + [UserAuth.isSignedIn], + (req, res, next) => { + getSemesterStudents(db, req.user, req.query.project_id) + .then((users) => { + res.send(users); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Get project members + router.get("/getProjectMembers", [UserAuth.isSignedIn], (req, res, next) => { + getProjectMembers(db, req.query.project_id) + .then((users) => { + res.send(users); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Get all active users + router.get("/getActiveUsers", [UserAuth.isAdmin], (req, res, next) => { + getActiveUsers(db) + .then((users) => { + res.send(users); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }); + + // Create a new user + router.post( + "/createUser", + [ + UserAuth.isAdmin, + UserAuth.canWrite, + body("system_id") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + body("fname") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + body("lname") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + body("email") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + body("type") + .not() + .isEmpty() + .trim() + .escape() + .withMessage("Cannot be empty") + .isLength({ max: 50 }), + body("semester_group").isLength({ max: 50 }), + body("project").isLength({ max: 50 }), + body("active").trim().escape().isLength({ max: 50 }), + body("viewOnly").trim().escape().isLength({ max: 50 }), + ], + async (req, res, next) => { + let result = validationResult(req); + console.log(result); + + if (result.errors.length !== 0) { + const error = new Error("Validation Error"); + error.statusCode = 400; + return next(error); + } + + createUser(db, req.body) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + // Batch create users + router.post( + "/batchCreateUser", + [UserAuth.isAdmin, UserAuth.canWrite], + async (req, res, next) => { + try { + let users = JSON.parse(req.body.users); + + batchCreateUser(db, users) + .then(({ successUsers, failedUsers }) => { + res.status(200).json({ successUsers, failedUsers }); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + } catch (err) { + const error = new Error(err); + error.statusCode = 500; + return next(error); + } + }, + ); + + // Edit user information + router.post( + "/editUser", + [UserAuth.isAdmin, UserAuth.canWrite], + (req, res, next) => { + editUser(db, req.body) + .then(() => { + return res.status(200).send(); + }) + .catch((err) => { + console.error(err); + const error = new Error(err); + error.statusCode = 500; + return next(error); + }); + }, + ); + + return router; +};