Skip to content

Commit

Permalink
Add specification to s3
Browse files Browse the repository at this point in the history
  • Loading branch information
Timothy-Gonzalez committed Oct 13, 2024
1 parent e40ce2c commit 6a1008a
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 72 deletions.
150 changes: 78 additions & 72 deletions src/services/s3/s3-router.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,89 @@
import { Request, Response, Router } from "express";
import { strongJwtVerification } from "../../middleware/verify-jwt";
import { JwtPayload } from "../auth/auth-schemas";
import { Router } from "express";
import { Role } from "../auth/auth-schemas";
import { StatusCode } from "status-code-enum";
import { hasElevatedPerms } from "../../common/auth";
import { getAuthenticatedUser } from "../../common/auth";
import { createSignedPostUrl, getSignedDownloadUrl } from "./s3-service";
import specification, { Tag } from "../../middleware/specification";
import Config from "../../common/config";
import { S3DownloadURLSchema, S3UploadURLSchema } from "./s3-schemas";
import { UserIdSchema } from "../user/user-schemas";
import { z } from "zod";

const s3Router = Router();

/**
* @api {get} /s3/upload GET /s3/upload
* @apiGroup s3
* @apiDescription Gets a presigned upload url to the resume s3 bucket for the currently authenticated user, valid for 60s.
*
* @apiSuccess (200: Success) {String} url presigned URL
*
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
"url": "https://resume-bucket-dev.s3.us-east-2.amazonaws.com/randomuser?randomstuffs",
}
*/
s3Router.get("/upload/", strongJwtVerification, async (_req: Request, res: Response) => {
const payload = res.locals.payload as JwtPayload;
const userId = payload.id;
s3Router.get(
"/upload/",
specification({
method: "get",
path: "/s3/upload/",
tag: Tag.S3,
role: Role.USER,
summary: "Gets a upload url for the resume of the currently authenticated user",
description: `This is a presigned url from s3 that is valid for ${Config.RESUME_URL_EXPIRY_SECONDS} seconds`,
responses: {
[StatusCode.SuccessOK]: {
description: "The upload url",
schema: S3UploadURLSchema,
},
},
}),
async (req, res) => {
const { id: userId } = getAuthenticatedUser(req);
const { url, fields } = await createSignedPostUrl(userId);
return res.status(StatusCode.SuccessOK).send({ url, fields });
},
);

const { url, fields } = await createSignedPostUrl(userId);
s3Router.get(
"/download/",
specification({
method: "get",
path: "/s3/download/",
tag: Tag.S3,
role: Role.USER,
summary: "Gets a download url for the resume of the currently authenticated user",
description: `This is a presigned url from s3 that is valid for ${Config.RESUME_URL_EXPIRY_SECONDS} seconds`,
responses: {
[StatusCode.SuccessOK]: {
description: "The download url",
schema: S3DownloadURLSchema,
},
},
}),
async (req, res) => {
const { id: userId } = getAuthenticatedUser(req);
const downloadUrl = await getSignedDownloadUrl(userId);
return res.status(StatusCode.SuccessOK).send({ url: downloadUrl });
},
);

return res.status(StatusCode.SuccessOK).send({ url: url, fields: fields });
});
s3Router.get(
"/download/:id",
specification({
method: "get",
path: "/s3/download/{id}",
tag: Tag.S3,
role: Role.ADMIN,
summary: "Gets a download url for the resume of the specified user",
description:
"Admin-only because this is for a specific user, use `GET /s3/download/` for the currently authenticated user",
parameters: z.object({
id: UserIdSchema,
}),
responses: {
[StatusCode.SuccessOK]: {
description: "The download url",
schema: S3DownloadURLSchema,
},
},
}),
async (req, res) => {
const userId = req.params.id;

/**
* @api {get} /s3/download GET /s3/download
* @apiGroup s3
* @apiDescription Gets a presigned download url for the resume of the currently authenticated user, valid for 60s.
*
* @apiSuccess (200: Success) {String} url presigned URL
*
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
"url": "https://resume-bucket-dev.s3.us-east-2.amazonaws.com/randomuser?randomstuffs",
}
*/
s3Router.get("/download/", strongJwtVerification, async (_req: Request, res: Response) => {
const payload = res.locals.payload as JwtPayload;
const userId = payload.id;
const downloadUrl = getSignedDownloadUrl(userId);
const downloadUrl = await getSignedDownloadUrl(userId);

return res.status(StatusCode.SuccessOK).send({ url: downloadUrl });
});

/**
* @api {get} /s3/download/:USERID GET /s3/download/:USERID
* @apiGroup s3
* @apiDescription Gets a presigned download url for the resume of the specified user, given that the caller has elevated perms
*
* @apiSuccess (200: Success) {String} url presigned URL
*
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
"url": "https://resume-bucket-dev.s3.us-east-2.amazonaws.com/randomuser?randomstuffs",
}
* @apiError (403: Forbidden) {String} Forbidden
* @apiErrorExample Example Error Response:
* HTTP/1.1 403 Forbidden
* {"error": "Forbidden"}
*/
s3Router.get("/download/:USERID", strongJwtVerification, async (req: Request, res: Response) => {
const userId = req.params.USERID as string;
const payload = res.locals.payload as JwtPayload;

if (!hasElevatedPerms(payload)) {
return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" });
}

const downloadUrl = await getSignedDownloadUrl(userId);

return res.status(StatusCode.SuccessOK).send({ url: downloadUrl });
});
return res.status(StatusCode.SuccessOK).send({ url: downloadUrl });
},
);

export default s3Router;
24 changes: 24 additions & 0 deletions src/services/s3/s3-schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from "zod";

export const S3DownloadURLSchema = z.object({
url: z.string().openapi({ example: "https://resume-bucket-dev.s3.us-east-2.amazonaws.com/abcd" }),
});

export const S3UploadURLSchema = S3DownloadURLSchema.extend({
fields: z.any(),
}).openapi("S3UploadURL", {
example: {
url: "https://resume-bucket-dev.s3.us-east-2.amazonaws.com/",
fields: {
success_action_status: "201",
"Content-Type": "application/pdf",
bucket: "resume-bucket-dev",
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
"X-Amz-Credential": "ABCD/20241013/us-east-2/s3/aws4_request",
"X-Amz-Date": "20241013T081251Z",
key: "github1234.pdf",
Policy: "eyJ==",
"X-Amz-Signature": "bfe6f0c382",
},
},
});

0 comments on commit 6a1008a

Please sign in to comment.