Skip to content

Commit

Permalink
Merge pull request #27 from ReflectionsProjections/dev/alex/s3-service
Browse files Browse the repository at this point in the history
S3 service for resume upload / download
  • Loading branch information
aletya authored Apr 24, 2024
2 parents 066b791 + 37c1efb commit e7b692d
Show file tree
Hide file tree
Showing 8 changed files with 8,604 additions and 39 deletions.
7,186 changes: 7,186 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@
"prettier": "3.2.5"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.496.0",
"@aws-sdk/s3-presigned-post": "^3.499.0",
"@aws-sdk/s3-request-presigner": "^3.496.0",
"@paralleldrive/cuid2": "^2.2.2",
"axios": "^1.6.8",
"@types/cors": "^2.8.17",
"aws-sdk": "^2.1604.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
Expand Down
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import authRouter from "./services/auth/auth-router";
import eventRouter from "./services/events/event-router";
import notificationsRouter from "./services/notifications/notifications-router";
import registrationRouter from "./services/registration/registration-router";
import s3Router from "./services/s3/s3-router";
import subscriptionRouter from "./services/subscription/subscription-router";

const app = express();
Expand All @@ -37,6 +38,7 @@ app.use("/auth", authRouter);
app.use("/event", eventRouter);
app.use("/notifications", notificationsRouter);
app.use("/registration", registrationRouter);
app.use("/s3", s3Router);
app.use("/subscription", subscriptionRouter);

app.get("/status", (_, res) => {
Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export const Config = {

JWT_SIGNING_SECRET: getEnv("JWT_SIGNING_SECRET"),
JWT_EXPIRATION_TIME: "1 day",

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

export const DeviceRedirects: Record<string, string> = {
Expand Down
12 changes: 6 additions & 6 deletions src/middleware/role-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function RoleChecker(

if (jwt == undefined) {
if (weakVerification) {
next();
return next();
}

return res.status(StatusCodes.BAD_REQUEST).json({ error: "NoJWT" });
Expand All @@ -33,29 +33,29 @@ export default function RoleChecker(
const userRoles = payload.roles;

if (weakVerification) {
next();
return next();
}

if (requiredRoles.length == 0) {
next();
return next();
}

// Admins (staff) can access any endpoint
if (userRoles.includes(Role.Enum.ADMIN)) {
next();
return next();
}

// Corporate role can access corporate only endpoints
if (requiredRoles.includes(Role.Enum.CORPORATE)) {
if (userRoles.includes(Role.Enum.CORPORATE)) {
next();
return next();
}
}

// Need to be a user to access user endpoints (app users)
if (requiredRoles.includes(Role.Enum.USER)) {
if (userRoles.includes(Role.Enum.USER)) {
next();
return next();
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/middleware/s3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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,
});

return next();
}
85 changes: 85 additions & 0 deletions src/services/s3/s3-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Request, Response, Router } from "express";
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";

const s3Router: Router = Router();

s3Router.get(
"/upload/",
RoleChecker([Role.enum.USER], false),
s3ClientMiddleware,
async (_req: Request, res: Response) => {
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 });
}
);

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

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

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

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

export default s3Router;
Loading

0 comments on commit e7b692d

Please sign in to comment.