Skip to content

Commit

Permalink
Add specification to mail
Browse files Browse the repository at this point in the history
  • Loading branch information
Timothy-Gonzalez committed Oct 13, 2024
1 parent 6a1008a commit 2b2862c
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 81 deletions.
8 changes: 4 additions & 4 deletions src/services/admission/admission-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getAsStaff, getAsUser, putAsStaff, putAsUser, getAsAttendee, putAsAppli
import { StatusCode } from "status-code-enum";
import type * as MailLib from "../../services/mail/mail-lib";
import type { AxiosResponse } from "axios";
import { MailInfoFormat } from "../mail/mail-formats";
import { MailInfo } from "../mail/mail-schemas";

const TESTER_DECISION = {
userId: TESTER.id,
Expand Down Expand Up @@ -107,7 +107,7 @@ describe("PUT /admission/update/", () => {
expect(sendMail).toBeCalledWith({
templateId: RegistrationTemplates.STATUS_UPDATE,
recipients: [], // empty because neither test case starts as status = TBD
} satisfies MailInfoFormat);
} satisfies MailInfo);

expect(retrievedEntries).toMatchObject(
expect.arrayContaining(
Expand Down Expand Up @@ -205,7 +205,7 @@ describe("PUT /admission/rsvp/accept", () => {
templateId: RegistrationTemplates.RSVP_CONFIRMATION,
recipients: [TESTER_APPLICATION.emailAddress],
subs: { name: TESTER_APPLICATION.preferredName },
} satisfies MailInfoFormat);
} satisfies MailInfo);

expect(stored).toMatchObject({
...TESTER_DECISION,
Expand Down Expand Up @@ -257,7 +257,7 @@ describe("PUT /admission/rsvp/decline/", () => {
expect(sendMail).toBeCalledWith({
templateId: RegistrationTemplates.RSVP_DECLINED,
recipients: [TESTER_APPLICATION.emailAddress],
} satisfies MailInfoFormat);
} satisfies MailInfo);

expect(stored).toMatchObject({
...TESTER_DECISION,
Expand Down
8 changes: 4 additions & 4 deletions src/services/admission/admission-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { StatusCode } from "status-code-enum";
import { NextFunction } from "express-serve-static-core";
import { RouterError } from "../../middleware/error-handler";
import { performRSVP } from "./admission-lib";
import { MailInfoFormat } from "../mail/mail-formats";
import { MailInfo } from "../mail/mail-schemas";
import { RegistrationTemplates } from "../../common/config";
import { getApplication } from "../registration/registration-lib";
import { sendMail } from "../mail/mail-lib";
Expand Down Expand Up @@ -123,7 +123,7 @@ admissionRouter.put("/rsvp/accept/", strongJwtVerification, async (_: Request, r
return next(new RouterError(StatusCode.ClientErrorNotFound, "ApplicationNotFound"));
}

let mailInfo: MailInfoFormat;
let mailInfo: MailInfo;
if (application.requestedTravelReimbursement && (queryResult.reimbursementValue ?? 0) > 0) {
mailInfo = {
templateId: RegistrationTemplates.RSVP_CONFIRMATION_WITH_REIMBURSE,
Expand Down Expand Up @@ -203,7 +203,7 @@ admissionRouter.put("/rsvp/decline/", strongJwtVerification, async (_: Request,
return next(new RouterError(StatusCode.ClientErrorNotFound, "ApplicationNotFound"));
}

const mailInfo: MailInfoFormat = {
const mailInfo: MailInfo = {
templateId: RegistrationTemplates.RSVP_DECLINED,
recipients: [application.emailAddress],
};
Expand Down Expand Up @@ -298,7 +298,7 @@ admissionRouter.put("/update/", strongJwtVerification, async (req: Request, res:
try {
await Promise.all(ops);

const mailInfo: MailInfoFormat = {
const mailInfo: MailInfo = {
templateId: RegistrationTemplates.STATUS_UPDATE,
recipients: recipients,
};
Expand Down
28 changes: 0 additions & 28 deletions src/services/mail/mail-formats.ts

This file was deleted.

23 changes: 3 additions & 20 deletions src/services/mail/mail-lib.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
import Config from "../../common/config";
import axios, { AxiosResponse } from "axios";
import { Response, NextFunction } from "express";
import { StatusCode } from "status-code-enum";
import { RouterError } from "../../middleware/error-handler";
import { MailInfoFormat } from "./mail-formats";
import { MailInfo, MailSendResults } from "./mail-schemas";

export async function sendMailWrapper(res: Response, next: NextFunction, mailInfo: MailInfoFormat): Promise<void | Response> {
try {
const result = await sendMail(mailInfo);
return res.status(StatusCode.SuccessOK).send(result.data);
} catch (error) {
return next(
new RouterError(StatusCode.ClientErrorBadRequest, "EmailNotSent", {
status: error.response?.status,
code: error.code,
}),
);
}
}

export function sendMail(mailInfo: MailInfoFormat): Promise<AxiosResponse> {
export function sendMail(mailInfo: MailInfo): Promise<AxiosResponse<MailSendResults>> {
const options = mailInfo.scheduleTime ? { start_time: mailInfo.scheduleTime } : {};
const recipients = mailInfo.recipients.map((emailAddress: string) => ({ address: `${emailAddress}` }));
const substitution_data = mailInfo.subs;
Expand All @@ -46,5 +29,5 @@ export function sendMail(mailInfo: MailInfoFormat): Promise<AxiosResponse> {
data: data,
};

return axios.post(Config.SPARKPOST_URL, data, config);
return axios.post<MailSendResults>(Config.SPARKPOST_URL, data, config);
}
60 changes: 39 additions & 21 deletions src/services/mail/mail-router.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,49 @@
// POST /registration/
// ➡️ send confirmation email to the email provided in application

import { NextFunction, Request, Response, Router } from "express";
import { strongJwtVerification } from "../../middleware/verify-jwt";
import { Router } from "express";
import { RouterError } from "../../middleware/error-handler";
import { StatusCode } from "status-code-enum";
import { hasElevatedPerms } from "../../common/auth";
import { JwtPayload } from "../auth/auth-schemas";
import { MailInfoFormat, isValidMailInfo } from "./mail-formats";
import { sendMailWrapper } from "./mail-lib";
import { Role } from "../auth/auth-schemas";
import { sendMail } from "./mail-lib";
import specification, { Tag } from "../../middleware/specification";
import { MailInfoSchema, MailSendResultsSchema } from "./mail-schemas";

const mailRouter = Router();

mailRouter.post("/send/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => {
const payload = res.locals.payload as JwtPayload;

if (!hasElevatedPerms(payload)) {
return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden"));
}

const mailInfo = req.body as MailInfoFormat;

if (!isValidMailInfo(mailInfo)) {
return next(new RouterError(StatusCode.ClientErrorBadRequest, "BadRequest"));
}

return sendMailWrapper(res, next, mailInfo);
});
mailRouter.post(
"/send/",
specification({
method: "post",
path: "/mail/send/",
tag: Tag.MAIL,
role: Role.ADMIN,
summary: "Sends an email",
description:
"**WARNING**: This endpoint is not very well documented, so make sure you know what you're doing before you use it directly.",
body: MailInfoSchema,
responses: {
[StatusCode.SuccessOK]: {
description: "The upload url",
schema: MailSendResultsSchema,
},
},
}),
async (req, res, next) => {
const mailInfo = req.body;

try {
const result = await sendMail(mailInfo);
return res.status(StatusCode.SuccessOK).json(result.data);
} catch (error) {
return next(
new RouterError(StatusCode.ClientErrorBadRequest, "EmailNotSent", {
status: error.response?.status,
code: error.code,
}),
);
}
},
);

export default mailRouter;
31 changes: 31 additions & 0 deletions src/services/mail/mail-schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from "zod";

export const MailInfoSchema = z
.object({
templateId: z.string(),
recipients: z.array(z.string()),
scheduleTime: z.optional(z.string()),
subs: z.optional(z.record(z.unknown())),
})
.openapi("MailInfo");

export type MailInfo = z.infer<typeof MailInfoSchema>;

export const MailSendResultsSchema = z
.object({
results: z.object({
total_rejected_recipients: z.number(),
total_accepted_recipients: z.number(),
id: z.string(),
}),
})
.openapi("MailSendResults", {
example: {
results: {
total_rejected_recipients: 0,
total_accepted_recipients: 1,
id: "11668787493850529",
},
},
});
export type MailSendResults = z.infer<typeof MailSendResultsSchema>;
4 changes: 2 additions & 2 deletions src/services/registration/registration-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { RegistrationFormat } from "./registration-formats";
import { Degree, Gender, HackInterest, HackOutreach, Race } from "./registration-models";
import type * as MailLib from "../../services/mail/mail-lib";
import type { AxiosResponse } from "axios";
import { MailInfoFormat } from "../../services/mail/mail-formats";
import { MailInfo } from "../mail/mail-schemas";

const APPLICATION = {
isProApplicant: false,
Expand Down Expand Up @@ -144,7 +144,7 @@ describe("POST /registration/submit/", () => {
templateId: RegistrationTemplates.REGISTRATION_SUBMISSION,
recipients: [UNSUBMITTED_REGISTRATION.emailAddress],
subs: { name: UNSUBMITTED_REGISTRATION.preferredName },
} satisfies MailInfoFormat);
} satisfies MailInfo);

// Stored in DB
const stored: RegistrationApplication | null = await Models.RegistrationApplication.findOne({
Expand Down
4 changes: 2 additions & 2 deletions src/services/registration/registration-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { hasElevatedPerms } from "../../common/auth";
import { JwtPayload } from "../auth/auth-schemas";

import { sendMail } from "../mail/mail-lib";
import { MailInfoFormat } from "../mail/mail-formats";
import { MailInfo } from "../mail/mail-schemas";
import { isRegistrationAlive } from "./registration-lib";

const registrationRouter = Router();
Expand Down Expand Up @@ -347,7 +347,7 @@ registrationRouter.post("/submit/", strongJwtVerification, async (_: Request, re
}

// SEND SUCCESSFUL REGISTRATION EMAIL
const mailInfo: MailInfoFormat = {
const mailInfo: MailInfo = {
templateId: RegistrationTemplates.REGISTRATION_SUBMISSION,
recipients: [registrationInfo.emailAddress],
subs: { name: registrationInfo.preferredName },
Expand Down

0 comments on commit 2b2862c

Please sign in to comment.