From 605719cdfd7f0e8e8eb4d52bb844517d8f8f879f Mon Sep 17 00:00:00 2001 From: Timothy-Gonzalez <105177619+Timothy-Gonzalez@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:18:31 -0500 Subject: [PATCH 1/2] Convert fcm middleware -> notification service --- src/middleware/fcm.ts | 20 --- .../notification/notification-router.ts | 124 ++++++++---------- .../notification/notification-service.ts | 22 ++++ 3 files changed, 80 insertions(+), 86 deletions(-) delete mode 100644 src/middleware/fcm.ts create mode 100644 src/services/notification/notification-service.ts diff --git a/src/middleware/fcm.ts b/src/middleware/fcm.ts deleted file mode 100644 index cc01ea7..0000000 --- a/src/middleware/fcm.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import Config from "../config"; -import admin, { ServiceAccount } from "firebase-admin"; - -export function NotificationsMiddleware(_: Request, res: Response, next: NextFunction): void { - const encodedKey = Config.FCM_SERVICE_ACCOUNT; - const serviceAccount = JSON.parse(atob(encodedKey)) as ServiceAccount; - const projectName = serviceAccount.projectId; - - if (!admin.apps.length) { - admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), - databaseURL: `https://${projectName}.firebaseio.com/`, - }); - ``; - } - - res.locals.fcm = admin; - next(); -} diff --git a/src/services/notification/notification-router.ts b/src/services/notification/notification-router.ts index 05daa89..3a6d8a3 100644 --- a/src/services/notification/notification-router.ts +++ b/src/services/notification/notification-router.ts @@ -7,8 +7,8 @@ import { JwtPayload } from "../auth/auth-models"; import { hasAdminPerms, hasStaffPerms } from "../auth/auth-lib"; import { NotificationSendFormat, isValidNotificationSendFormat } from "./notification-formats"; import { StaffShift } from "database/staff-db"; -import { NotificationsMiddleware } from "../../middleware/fcm"; import Config from "../../config"; +import { sendNotification } from "./notification-service"; const notificationsRouter = Router(); @@ -102,72 +102,64 @@ notificationsRouter.post("/batch/", strongJwtVerification, async (req: Request, // Sends notifications to a batch of users, gotten from /notification/batch // Only accepts Config.NOTIFICATION_BATCH_SIZE users. -notificationsRouter.post( - "/send", - strongJwtVerification, - NotificationsMiddleware, - async (req: Request, res: Response, next: NextFunction) => { - const payload = res.locals.payload as JwtPayload; - const admin = res.locals.fcm; - const sendRequest = req.body as { batchId: string }; - - if (!hasAdminPerms(payload)) { - return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); - } - if (!sendRequest.batchId) { - return next(new RouterError(StatusCode.ClientErrorBadRequest, "NoBatchId")); - } - - const decodedBatchId = Buffer.from(sendRequest.batchId, "base64url").toString("utf-8"); - const [messageId, targetUserIds] = JSON.parse(decodedBatchId) as [string, string[]]; - - const message = await Models.NotificationMessages.findById(messageId); - - if (!message || !targetUserIds || targetUserIds.length > Config.NOTIFICATION_BATCH_SIZE) { - return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidBatch")); - } - const messageTemplate = { - notification: { - title: message.title, - body: message.body, - }, - }; - const startTime = new Date(); - let notifMappings = await Models.NotificationMappings.find({ userId: { $in: targetUserIds } }).exec(); - notifMappings = notifMappings.filter((x) => x?.deviceToken != undefined); - - const sent: string[] = []; - const failed: string[] = []; - - const messages = notifMappings.map((mapping) => - admin - .messaging() - .send({ token: mapping.deviceToken, ...messageTemplate }) - .then(() => { - sent.push(mapping.userId); - }) - .catch(() => { - failed.push(mapping.userId); - }), - ); - await Promise.all(messages); - await Models.NotificationMessages.findOneAndUpdate( - { - _id: messageId, - }, - { - $push: { - batches: { - sent, - failed, - }, +notificationsRouter.post("/send", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { + const payload = res.locals.payload as JwtPayload; + const sendRequest = req.body as { batchId: string }; + + if (!hasAdminPerms(payload)) { + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); + } + if (!sendRequest.batchId) { + return next(new RouterError(StatusCode.ClientErrorBadRequest, "NoBatchId")); + } + + const decodedBatchId = Buffer.from(sendRequest.batchId, "base64url").toString("utf-8"); + const [messageId, targetUserIds] = JSON.parse(decodedBatchId) as [string, string[]]; + + const message = await Models.NotificationMessages.findById(messageId); + + if (!message || !targetUserIds || targetUserIds.length > Config.NOTIFICATION_BATCH_SIZE) { + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidBatch")); + } + const messageTemplate = { + notification: { + title: message.title, + body: message.body, + }, + }; + const startTime = new Date(); + let notifMappings = await Models.NotificationMappings.find({ userId: { $in: targetUserIds } }).exec(); + notifMappings = notifMappings.filter((x) => x?.deviceToken != undefined); + + const sent: string[] = []; + const failed: string[] = []; + + const messages = notifMappings.map((mapping) => + sendNotification({ token: mapping.deviceToken, ...messageTemplate }) + .then(() => { + sent.push(mapping.userId); + }) + .catch(() => { + failed.push(mapping.userId); + }), + ); + await Promise.all(messages); + await Models.NotificationMessages.findOneAndUpdate( + { + _id: messageId, + }, + { + $push: { + batches: { + sent, + failed, }, }, - ); - const endTime = new Date(); - const timeElapsed = endTime.getTime() - startTime.getTime(); - return res.status(StatusCode.SuccessOK).send({ status: "Success", sent, failed, time_ms: timeElapsed }); - }, -); + }, + ); + const endTime = new Date(); + const timeElapsed = endTime.getTime() - startTime.getTime(); + return res.status(StatusCode.SuccessOK).send({ status: "Success", sent, failed, time_ms: timeElapsed }); +}); export default notificationsRouter; diff --git a/src/services/notification/notification-service.ts b/src/services/notification/notification-service.ts new file mode 100644 index 0000000..4c8b383 --- /dev/null +++ b/src/services/notification/notification-service.ts @@ -0,0 +1,22 @@ +import { Message } from "firebase-admin/lib/messaging/messaging-api"; +import Config from "../../config"; +import admin, { ServiceAccount } from "firebase-admin"; + +function initializeFCM(): void { + if (!admin.apps.length) { + const encodedKey = Config.FCM_SERVICE_ACCOUNT; + const serviceAccount = JSON.parse(atob(encodedKey)) as ServiceAccount; + const projectName = serviceAccount.projectId; + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + databaseURL: `https://${projectName}.firebaseio.com/`, + }); + ``; + } +} + +export function sendNotification(message: Message): Promise { + initializeFCM(); + + return admin.messaging().send(message); +} From 913a729e2657e5a70324f23cb945921006466b35 Mon Sep 17 00:00:00 2001 From: Timothy-Gonzalez <105177619+Timothy-Gonzalez@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:28:11 -0500 Subject: [PATCH 2/2] Convert s3 middleware to service --- src/middleware/s3.ts | 17 ------------ src/services/s3/s3-router.ts | 50 ++++++----------------------------- src/services/s3/s3-service.ts | 50 +++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 59 deletions(-) delete mode 100644 src/middleware/s3.ts create mode 100644 src/services/s3/s3-service.ts diff --git a/src/middleware/s3.ts b/src/middleware/s3.ts deleted file mode 100644 index 4960304..0000000 --- a/src/middleware/s3.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import { S3 } from "@aws-sdk/client-s3"; - -import Config from "../config"; - -export function s3ClientMiddleware(_: Request, res: Response, next: NextFunction): void { - res.locals.s3 = new S3({ - apiVersion: "2006-03-01", - credentials: { - accessKeyId: Config.S3_ACCESS_KEY, - secretAccessKey: Config.S3_SECRET_KEY, - }, - region: Config.S3_REGION, - }); - - next(); -} diff --git a/src/services/s3/s3-router.ts b/src/services/s3/s3-router.ts index de1d4ae..2840063 100644 --- a/src/services/s3/s3-router.ts +++ b/src/services/s3/s3-router.ts @@ -3,12 +3,7 @@ import { strongJwtVerification } from "../../middleware/verify-jwt"; import { JwtPayload } from "../auth/auth-models"; import { StatusCode } from "status-code-enum"; import { hasElevatedPerms } from "../auth/auth-lib"; - -import Config from "../../config"; -import { GetObjectCommand, type S3 } from "@aws-sdk/client-s3"; -import { s3ClientMiddleware } from "../../middleware/s3"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { createPresignedPost } from "@aws-sdk/s3-presigned-post"; +import { createSignedPostUrl, getSignedDownloadUrl } from "./s3-service"; const s3Router = Router(); @@ -25,23 +20,11 @@ const s3Router = Router(); "url": "https://resume-bucket-dev.s3.us-east-2.amazonaws.com/randomuser?randomstuffs", } */ -s3Router.get("/upload/", strongJwtVerification, s3ClientMiddleware, async (_req: Request, res: Response) => { +s3Router.get("/upload/", strongJwtVerification, async (_req: Request, res: Response) => { const payload = res.locals.payload as JwtPayload; - const s3 = res.locals.s3 as S3; const userId = payload.id; - const { url, fields } = await createPresignedPost(s3, { - Bucket: Config.S3_BUCKET_NAME, - Key: `${userId}.pdf`, - Conditions: [ - ["content-length-range", 0, Config.MAX_RESUME_SIZE_BYTES], // 5 MB max - ], - Fields: { - success_action_status: "201", - "Content-Type": "application/pdf", - }, - Expires: Config.RESUME_URL_EXPIRY_SECONDS, - }); + const { url, fields } = await createSignedPostUrl(userId); return res.status(StatusCode.SuccessOK).send({ url: url, fields: fields }); }); @@ -59,19 +42,10 @@ s3Router.get("/upload/", strongJwtVerification, s3ClientMiddleware, async (_req: "url": "https://resume-bucket-dev.s3.us-east-2.amazonaws.com/randomuser?randomstuffs", } */ -s3Router.get("/download/", strongJwtVerification, s3ClientMiddleware, async (_req: Request, res: Response) => { +s3Router.get("/download/", strongJwtVerification, async (_req: Request, res: Response) => { const payload = res.locals.payload as JwtPayload; - const s3 = res.locals.s3 as S3; const userId = payload.id; - - const command = new GetObjectCommand({ - Bucket: Config.S3_BUCKET_NAME, - Key: `${userId}.pdf`, - }); - - const downloadUrl = await getSignedUrl(s3, command, { - expiresIn: Config.RESUME_URL_EXPIRY_SECONDS, - }); + const downloadUrl = getSignedDownloadUrl(userId); return res.status(StatusCode.SuccessOK).send({ url: downloadUrl }); }); @@ -93,23 +67,15 @@ s3Router.get("/download/", strongJwtVerification, s3ClientMiddleware, async (_re * HTTP/1.1 403 Forbidden * {"error": "Forbidden"} */ -s3Router.get("/download/:USERID", strongJwtVerification, s3ClientMiddleware, async (req: Request, res: Response) => { - const userId = req.params.USERID; +s3Router.get("/download/:USERID", strongJwtVerification, async (req: Request, res: Response) => { + const userId = req.params.USERID as string; const payload = res.locals.payload as JwtPayload; - const s3 = res.locals.s3 as S3; if (!hasElevatedPerms(payload)) { return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); } - const command = new GetObjectCommand({ - Bucket: Config.S3_BUCKET_NAME, - Key: `${userId}.pdf`, - }); - - const downloadUrl = await getSignedUrl(s3, command, { - expiresIn: Config.RESUME_URL_EXPIRY_SECONDS, - }); + const downloadUrl = await getSignedDownloadUrl(userId); return res.status(StatusCode.SuccessOK).send({ url: downloadUrl }); }); diff --git a/src/services/s3/s3-service.ts b/src/services/s3/s3-service.ts new file mode 100644 index 0000000..04b753d --- /dev/null +++ b/src/services/s3/s3-service.ts @@ -0,0 +1,50 @@ +import { GetObjectCommand, S3 } from "@aws-sdk/client-s3"; +import Config from "../../config"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { createPresignedPost, PresignedPost } from "@aws-sdk/s3-presigned-post"; + +let s3: S3 | undefined = undefined; + +function getClient(): S3 { + if (!s3) { + s3 = new S3({ + apiVersion: "2006-03-01", + credentials: { + accessKeyId: Config.S3_ACCESS_KEY, + secretAccessKey: Config.S3_SECRET_KEY, + }, + region: Config.S3_REGION, + }); + } + return s3; +} + +export function getSignedDownloadUrl(userId: string): Promise { + const s3 = getClient(); + + const command = new GetObjectCommand({ + Bucket: Config.S3_BUCKET_NAME, + Key: `${userId}.pdf`, + }); + + return getSignedUrl(s3, command, { + expiresIn: Config.RESUME_URL_EXPIRY_SECONDS, + }); +} + +export function createSignedPostUrl(userId: string): Promise { + const s3 = getClient(); + + return createPresignedPost(s3, { + Bucket: Config.S3_BUCKET_NAME, + Key: `${userId}.pdf`, + Conditions: [ + ["content-length-range", 0, Config.MAX_RESUME_SIZE_BYTES], // 5 MB max + ], + Fields: { + success_action_status: "201", + "Content-Type": "application/pdf", + }, + Expires: Config.RESUME_URL_EXPIRY_SECONDS, + }); +}