Skip to content

Commit

Permalink
s3 flexible
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobc2700 committed Jul 15, 2024
1 parent 25b50bd commit deca9dd
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 49 deletions.
7 changes: 6 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export const Environment = z.enum(["PRODUCTION", "DEVELOPMENT", "TESTING"]);

export const MailingListName = z.enum(["rp_interest"]);

// Native enum for bucket names
export enum BucketName {
RP_2024_RESUMES = "rp-2024-resumes",
RP_2024_SPEAKERS = "rp-2024-speakers",
}

export const Config = {
DEFAULT_APP_PORT: 3000,
ALLOWED_CORS_ORIGIN_PATTERNS: [
Expand Down Expand Up @@ -71,7 +77,6 @@ export const Config = {

S3_ACCESS_KEY: getEnv("S3_ACCESS_KEY"),
S3_SECRET_KEY: getEnv("S3_SECRET_KEY"),
S3_BUCKET_NAME: getEnv("S3_BUCKET_NAME"),
S3_REGION: getEnv("S3_REGION"),
MAX_RESUME_SIZE_BYTES: 6 * 1024 * 1024,
RESUME_URL_EXPIRY_SECONDS: 60,
Expand Down
22 changes: 21 additions & 1 deletion src/services/attendees/attendee-router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import { AttendeeValidator, EventIdValidator } from "./attendee-schema";
import {
AttendeeValidator,
EventIdValidator,
PartialAttendeeFilter,
} from "./attendee-schema";
import { Database } from "../../database";
import RoleChecker from "../../middleware/role-checker";
import { Role } from "../auth/auth-models";
Expand Down Expand Up @@ -150,4 +154,20 @@ attendeeRouter.get(
}
);

// Get attendees based on a partial filter in body
attendeeRouter.post(
"/filter",
RoleChecker([Role.Enum.ADMIN]),
async (req, res, next) => {
try {
const attendeeData = PartialAttendeeFilter.parse(req.body);
const attendees = await Database.ATTENDEES.find(attendeeData);

return res.status(StatusCodes.OK).json(attendees);
} catch (error) {
next(error);
}
}
);

export default attendeeRouter;
10 changes: 9 additions & 1 deletion src/services/attendees/attendee-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,12 @@ const EventIdValidator = z.object({
eventId: z.string().uuid(),
});

export { AttendeeSchema, AttendeeValidator, EventIdValidator };
// Partial schema for attendee filter
const PartialAttendeeFilter = AttendeeValidator.partial();

export {
AttendeeSchema,
AttendeeValidator,
EventIdValidator,
PartialAttendeeFilter,
};
127 changes: 81 additions & 46 deletions src/services/s3/s3-router.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,119 @@
import { Request, Response, Router } from "express";
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import RoleChecker from "../../middleware/role-checker";
import { s3ClientMiddleware } from "../../middleware/s3";
import { StatusCodes } from "http-status-codes";
import { Config } from "../../config";
import { Role } from "../auth/auth-models";

import { GetObjectCommand, S3 } from "@aws-sdk/client-s3";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { S3 } from "@aws-sdk/client-s3";
import { getResumeUrl, postResumeUrl } from "./s3-utils";
import BatchResumeDownloadValidator from "./s3-schema";

Check failure on line 9 in src/services/s3/s3-router.ts

View workflow job for this annotation

GitHub Actions / build

Cannot find module './s3-schema' or its corresponding type declarations.
import { BucketName } from "../../config";

const s3Router: Router = Router();

s3Router.get(
"/upload/",
RoleChecker([], false),
s3ClientMiddleware,
async (_req: Request, res: Response) => {
async (_req, res, next) => {
const payload = res.locals.payload;

const s3 = res.locals.s3 as S3;
const userId: string = payload.userId;

const { url, fields } = await createPresignedPost(s3, {
Bucket: Config.S3_BUCKET_NAME,
Key: `${userId}.pdf`,
Conditions: [
["content-length-range", 0, Config.MAX_RESUME_SIZE_BYTES], // 6 MB max
],
Fields: {
success_action_status: "201",
"Content-Type": "application/pdf",
},
Expires: Config.RESUME_URL_EXPIRY_SECONDS,
});

return res.status(StatusCodes.OK).send({ url: url, fields: fields });
try {
const { url, fields } = await postResumeUrl(
userId,
s3,
BucketName.RP_2024_RESUMES
);
return res.status(StatusCodes.OK).send({ url, fields });
} catch (error) {
next(error);
}
}
);

s3Router.get(
"/download/",
RoleChecker([Role.enum.USER], false),
RoleChecker([Role.Enum.USER], false),
s3ClientMiddleware,
async (_req: Request, res: Response) => {
async (_, res, next) => {
const payload = res.locals.payload;
const userId = payload.userId;

const s3 = res.locals.s3 as S3;
const userId: string = payload.userId;

const command = new GetObjectCommand({
Bucket: Config.S3_BUCKET_NAME,
Key: `${userId}.pdf`,
});

const downloadUrl = await getSignedUrl(s3, command, {
expiresIn: Config.RESUME_URL_EXPIRY_SECONDS,
});

return res.status(StatusCodes.OK).send({ url: downloadUrl });
try {
const downloadUrl = await getResumeUrl(
userId,
s3,
BucketName.RP_2024_RESUMES
);
return res.status(StatusCodes.OK).send({ url: downloadUrl });
} catch (error) {
next(error);
}
}
);

s3Router.get(
"/download/:USERID",
RoleChecker([Role.enum.STAFF], false),
"/download/user/:USERID",
RoleChecker([Role.Enum.STAFF, Role.Enum.CORPORATE], false),
s3ClientMiddleware,
async (req: Request, res: Response) => {
const userId: string = req.params.USERID;
async (req, res, next) => {
const userId = req.params.USERID;
const s3 = res.locals.s3 as S3;

const command = new GetObjectCommand({
Bucket: Config.S3_BUCKET_NAME,
Key: `${userId}.pdf`,
});
try {
const downloadUrl = await getResumeUrl(
userId,
s3,
BucketName.RP_2024_RESUMES
);
return res.status(StatusCodes.OK).send({ url: downloadUrl });
} catch (error) {
next(error);
}
}
);

const downloadUrl = await getSignedUrl(s3, command, {
expiresIn: Config.RESUME_URL_EXPIRY_SECONDS,
});
s3Router.get(
"/download/batch/",
RoleChecker([Role.Enum.STAFF, Role.Enum.CORPORATE], false),
s3ClientMiddleware,
async (req, res, next) => {
const s3 = res.locals.s3 as S3;

return res.status(StatusCodes.OK).send({ url: downloadUrl });
try {
const { userIds } = BatchResumeDownloadValidator.parse(req.body);

const batchDownloadPromises = userIds.map((userId) =>

Check failure on line 91 in src/services/s3/s3-router.ts

View workflow job for this annotation

GitHub Actions / build

Parameter 'userId' implicitly has an 'any' type.
getResumeUrl(userId, s3, BucketName.RP_2024_RESUMES)
.then((url) => ({ userId, url: url }))
.catch(() => ({ userId, url: null }))
);

const batchDownloadResults = await Promise.allSettled(
batchDownloadPromises
);

const filteredUrls = batchDownloadResults.forEach((result) => {

Check failure on line 101 in src/services/s3/s3-router.ts

View workflow job for this annotation

GitHub Actions / build

Parameter 'result' implicitly has an 'any' type.
if (result.status === "fulfilled") {
return result.value;
}
});

const errors = batchDownloadResults.filter(
(result) => result.status === "rejected"

Check failure on line 108 in src/services/s3/s3-router.ts

View workflow job for this annotation

GitHub Actions / build

Parameter 'result' implicitly has an 'any' type.
).length;

return res
.status(StatusCodes.OK)
.send({ data: filteredUrls, errorCount: errors });
} catch (error) {
next(error);
}
}
);

Expand Down
40 changes: 40 additions & 0 deletions src/services/s3/s3-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GetObjectCommand, S3 } from "@aws-sdk/client-s3";
import Config, { BucketName } from "../../config";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";

export async function postResumeUrl(
userId: string,
client: S3,
bucketName: BucketName
) {
const { url, fields } = await createPresignedPost(client, {
Bucket: bucketName,
Key: `${userId}.pdf`,
Conditions: [
["content-length-range", 0, Config.MAX_RESUME_SIZE_BYTES], // 6 MB max
],
Fields: {
success_action_status: "201",
"Content-Type": "application/pdf",
},
Expires: Config.RESUME_URL_EXPIRY_SECONDS,
});

return { url, fields };
}

export async function getResumeUrl(
userId: string,
client: S3,
bucketName: BucketName
) {
const command = new GetObjectCommand({
Bucket: bucketName,
Key: `${userId}.pdf`,
});

return getSignedUrl(client, command, {
expiresIn: Config.RESUME_URL_EXPIRY_SECONDS,
});
}

0 comments on commit deca9dd

Please sign in to comment.