Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup middlewares #227

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions src/middleware/fcm.ts

This file was deleted.

17 changes: 0 additions & 17 deletions src/middleware/s3.ts

This file was deleted.

124 changes: 58 additions & 66 deletions src/services/notification/notification-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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;
22 changes: 22 additions & 0 deletions src/services/notification/notification-service.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
initializeFCM();

return admin.messaging().send(message);
}
50 changes: 8 additions & 42 deletions src/services/s3/s3-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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 });
});
Expand All @@ -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 });
});
Expand All @@ -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 });
});
Expand Down
50 changes: 50 additions & 0 deletions src/services/s3/s3-service.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<PresignedPost> {
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,
});
}
Loading