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, + }); +}