diff --git a/src/app.ts b/src/app.ts index 6da05cb..4502034 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,6 +12,7 @@ import errorHandler from "./middleware/error-handler"; import attendeeRouter from "./services/attendees/attendee-router"; import authRouter from "./services/auth/auth-router"; import eventsRouter from "./services/events/events-router"; +import mailRouter from "./services/mail/mail-router"; import notificationsRouter from "./services/notifications/notifications-router"; import registrationRouter from "./services/registration/registration-router"; import s3Router from "./services/s3/s3-router"; @@ -36,6 +37,7 @@ app.use("/", bodyParser.json()); app.use("/attendee", attendeeRouter); app.use("/auth", authRouter); app.use("/events", eventsRouter); +app.use("/mail", mailRouter); app.use("/notifications", notificationsRouter); app.use("/registration", registrationRouter); app.use("/s3", s3Router); diff --git a/src/config.ts b/src/config.ts index b92f607..e968b3a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,6 +29,13 @@ export const Config = { AUTH_CALLBACK_URI_BASE: "https://api.reflectionsprojections.org/auth/callback/", + AUTH_ADMIN_WHITELIST: new Set([ + "apirani2@illinois.edu", // Aydan Pirani (Dev) + "divyack2@illinois.edu", // Divya Koya (Dev) + "ritikav2@illinois.edu", // Ritika Vithani (Director) + "ojaswee2@illinois.edu", // Ojaswee Chaudhary (Director) + ]), + JWT_SIGNING_SECRET: getEnv("JWT_SIGNING_SECRET"), JWT_EXPIRATION_TIME: "1 day", @@ -42,6 +49,8 @@ export const Config = { // QR Scanning QR_HASH_ITERATIONS: 10000, QR_HASH_SECRET: getEnv("QR_HASH_SECRET"), + + MAIL_TEMPLATE_REGEX: /\${{([^{}]+)}}/g, }; export const DeviceRedirects: Record = { diff --git a/src/database.ts b/src/database.ts index 460279a..b38e452 100644 --- a/src/database.ts +++ b/src/database.ts @@ -17,6 +17,10 @@ import { NotificationsSchema, NotificationsValidator, } from "./services/notifications/notifications-schema"; +import { + TemplateSchema, + TemplateValidator, +} from "./services/mail/templates/templates-schema"; mongoose.set("toObject", { versionKey: false }); @@ -61,6 +65,7 @@ export const Database = { RegistrationSchema, RegistrationValidator ), + TEMPLATES: initializeModel("templates", TemplateSchema, TemplateValidator), NOTIFICATIONS: initializeModel( "notifications", NotificationsSchema, diff --git a/src/services/auth/auth-utils.ts b/src/services/auth/auth-utils.ts index b4a3c2e..31df201 100644 --- a/src/services/auth/auth-utils.ts +++ b/src/services/auth/auth-utils.ts @@ -2,6 +2,7 @@ import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Config } from "../../config"; import { Database } from "../../database"; +import { Role } from "./auth-models"; export function createGoogleStrategy(device: string) { return new GoogleStrategy( @@ -17,9 +18,15 @@ export function createGoogleStrategy(device: string) { const name = profile.displayName; const email = profile._json.email; + let roles = []; + + if (Config.AUTH_ADMIN_WHITELIST.has(email ?? "")) { + roles.push(Role.Values.ADMIN); + } + Database.ROLES.findOneAndUpdate( { userId: userId }, - { userId, name, email }, + { userId, name, email, roles }, { upsert: true } ) .then(() => cb(null, profile)) diff --git a/src/services/mail/drafts/drafts-subrouter.ts b/src/services/mail/drafts/drafts-subrouter.ts new file mode 100644 index 0000000..ce98f35 --- /dev/null +++ b/src/services/mail/drafts/drafts-subrouter.ts @@ -0,0 +1,5 @@ +import { Router } from "express"; + +const draftsSubRouter = Router(); + +export default draftsSubRouter; diff --git a/src/services/mail/lists/lists-schema.ts b/src/services/mail/lists/lists-schema.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/mail/lists/lists-subrouter.ts b/src/services/mail/lists/lists-subrouter.ts new file mode 100644 index 0000000..6949191 --- /dev/null +++ b/src/services/mail/lists/lists-subrouter.ts @@ -0,0 +1,5 @@ +import { Router } from "express"; + +const listsSubRouter = Router(); + +export default listsSubRouter; diff --git a/src/services/mail/mail-router.ts b/src/services/mail/mail-router.ts new file mode 100644 index 0000000..ede8596 --- /dev/null +++ b/src/services/mail/mail-router.ts @@ -0,0 +1,13 @@ +import draftsSubRouter from "./drafts/drafts-subrouter"; +import listsSubRouter from "./lists/lists-subrouter"; +import templatesSubRouter from "./templates/templates-subrouter"; + +import { Router } from "express"; + +const mailRouter = Router(); + +mailRouter.use("/drafts", draftsSubRouter); +mailRouter.use("/lists", listsSubRouter); +mailRouter.use("/templates", templatesSubRouter); + +export default mailRouter; diff --git a/src/services/mail/templates/templates-schema.ts b/src/services/mail/templates/templates-schema.ts new file mode 100644 index 0000000..f1542c7 --- /dev/null +++ b/src/services/mail/templates/templates-schema.ts @@ -0,0 +1,31 @@ +import { Schema } from "mongoose"; +import { z } from "zod"; + +export const TemplateValidator = z.object({ + templateId: z.string().regex(/^\S*$/, { + message: "Spaces Not Allowed", + }), + subject: z.string(), + content: z.string(), + substitutions: z.string().array().default([]), +}); + +export const TemplateSchema = new Schema({ + templateId: { + type: String, + required: true, + unique: true, + }, + subject: { + type: String, + required: true, + }, + content: { + type: String, + required: true, + }, + substitutions: { + type: [String], + required: true, + }, +}); diff --git a/src/services/mail/templates/templates-subrouter.ts b/src/services/mail/templates/templates-subrouter.ts new file mode 100644 index 0000000..6333076 --- /dev/null +++ b/src/services/mail/templates/templates-subrouter.ts @@ -0,0 +1,111 @@ +import { Router } from "express"; +import RoleChecker from "../../../middleware/role-checker"; +import { Role } from "../../auth/auth-models"; +import { Database } from "../../../database"; +import { StatusCodes } from "http-status-codes"; +import { TemplateValidator } from "./templates-schema"; +import { Config } from "../../../config"; + +const templatesSubRouter = Router(); + +templatesSubRouter.get( + "/", + RoleChecker([Role.Values.STAFF]), + async (req, res) => { + const templates = await Database.TEMPLATES.find(); + const cleanedTemplates = templates.map((x) => x.toObject()); + return res.status(StatusCodes.OK).json(cleanedTemplates); + } +); + +templatesSubRouter.post( + "/", + RoleChecker([Role.Values.ADMIN]), + async (req, res) => { + try { + let templateData = TemplateValidator.parse(req.body); + let substitutions = templateData.content.matchAll( + Config.MAIL_TEMPLATE_REGEX + ); + const subVars = Array.from( + substitutions, + (substitutions) => substitutions[1] + ); + + await Database.TEMPLATES.create({ + ...templateData, + substitutions: subVars, + }); + return res.sendStatus(StatusCodes.CREATED); + } catch (error) { + return res.status(StatusCodes.BAD_REQUEST).send(error); + } + } +); + +templatesSubRouter.put( + "/", + RoleChecker([Role.Values.ADMIN]), + async (req, res) => { + try { + let templateData = TemplateValidator.parse(req.body); + let substitutions = templateData.content.matchAll( + Config.MAIL_TEMPLATE_REGEX + ); + const subVars = Array.from( + substitutions, + (substitutions) => substitutions[1] + ); + + const updateResult = await Database.TEMPLATES.findOneAndUpdate( + { templateId: templateData.templateId }, + { ...templateData, substitutions: subVars } + ); + + if (!updateResult) { + return res + .status(StatusCodes.NOT_FOUND) + .send({ error: "NoSuchId" }); + } + + return res.sendStatus(StatusCodes.OK); + } catch (error) { + return res.status(StatusCodes.BAD_REQUEST).send(error); + } + } +); + +templatesSubRouter.get( + "/:TEMPLATEID", + RoleChecker([Role.Values.STAFF]), + async (req, res) => { + const templateId = req.params.TEMPLATEID; + const templateInfo = await Database.TEMPLATES.findOne({ + templateId: templateId, + }); + if (!templateInfo) { + return res + .status(StatusCodes.NOT_FOUND) + .send({ error: "NoSuchId" }); + } + return res.status(StatusCodes.OK).json(templateInfo?.toObject()); + } +); + +templatesSubRouter.delete( + "/:TEMPLATEID", + RoleChecker([Role.Values.ADMIN]), + async (req, res) => { + try { + const templateId = req.params.TEMPLATEID; + await Database.TEMPLATES.findOneAndDelete({ + templateId: templateId, + }); + return res.sendStatus(StatusCodes.NO_CONTENT); + } catch (error) { + return res.status(StatusCodes.BAD_REQUEST).send(error); + } + } +); + +export default templatesSubRouter;