Skip to content

Commit 782ba3f

Browse files
committed
stuff
1 parent 04f83f0 commit 782ba3f

File tree

10 files changed

+280
-5
lines changed

10 files changed

+280
-5
lines changed

cloudformation/iam.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ Resources:
5454
Resource:
5555
- !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:infra-events-api-config*
5656
PolicyName: lambda-db-secrets
57+
- PolicyDocument:
58+
Version: 2012-10-17
59+
Statement:
60+
- Action:
61+
- dynamodb:*
62+
Effect: Allow
63+
Resource:
64+
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-api-records
65+
PolicyName: lambda-dynamo
5766
Outputs:
5867
MainFunctionRoleArn:
5968
Description: Main API IAM role ARN

cloudformation/main.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ Resources:
111111
Path: /{proxy+}
112112
Method: ANY
113113

114+
EventRecordsTable:
115+
Type: 'AWS::DynamoDB::Table'
116+
DeletionPolicy: "Retain"
117+
Properties:
118+
BillingMode: 'PAY_PER_REQUEST'
119+
TableName: infra-events-api-records
120+
DeletionProtectionEnabled: true
121+
PointInTimeRecoverySpecification:
122+
PointInTimeRecoveryEnabled: !If [IsProd, true, false]
123+
AttributeDefinitions:
124+
- AttributeName: id
125+
AttributeType: S
126+
KeySchema:
127+
- AttributeName: id
128+
KeyType: HASH
129+
114130
AppApiGateway:
115131
Type: AWS::Serverless::Api
116132
DependsOn:

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@
3434
"typescript": "^5.5.4"
3535
},
3636
"dependencies": {
37+
"@aws-sdk/client-dynamodb": "^3.624.0",
3738
"@aws-sdk/client-secrets-manager": "^3.624.0",
39+
"@aws-sdk/util-dynamodb": "^3.624.0",
3840
"@fastify/auth": "^4.6.1",
3941
"@fastify/aws-lambda": "^4.1.0",
4042
"fastify": "^4.28.1",
4143
"fastify-plugin": "^4.5.1",
4244
"jsonwebtoken": "^9.0.2",
43-
"jwks-rsa": "^3.1.0"
45+
"jwks-rsa": "^3.1.0",
46+
"zod": "^3.23.8",
47+
"zod-to-json-schema": "^3.23.2"
4448
}
4549
}

src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
DYNAMO_TABLE_NAME: "infra-events-api-records",
3+
};

src/errors/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,25 @@ export class NotFoundError extends BaseError<"NotFoundError"> {
7070
});
7171
}
7272
}
73+
74+
export class ValidationError extends BaseError<"ValidationError"> {
75+
constructor({ message }: { message: string }) {
76+
super({
77+
name: "ValidationError",
78+
id: 104,
79+
message,
80+
httpStatusCode: 400,
81+
});
82+
}
83+
}
84+
85+
export class DatabaseInsertError extends BaseError<"DatabaseInsertError"> {
86+
constructor({ message }: { message: string }) {
87+
super({
88+
name: "DatabaseInsertError",
89+
id: 105,
90+
message,
91+
httpStatusCode: 500,
92+
});
93+
}
94+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import protectedRoute from "./routes/protected.js";
77
import errorHandlerPlugin from "./plugins/errorHandler.js";
88
import { RunEnvironment, runEnvironments } from "./roles.js";
99
import { InternalServerError } from "./errors/index.js";
10+
import createEvent from "./routes/event.js";
1011

1112
const now = () => Date.now();
1213

@@ -57,6 +58,7 @@ async function init() {
5758
await app.register(
5859
async (api, _options) => {
5960
api.register(protectedRoute, { prefix: "/protected" });
61+
api.register(createEvent, { prefix: "/event" });
6062
},
6163
{ prefix: "/api/v1" },
6264
);

src/orgs.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const SIGList = [
2+
"SIGPwny",
3+
"SIGCHI",
4+
"GameBuilders",
5+
"SIGAIDA",
6+
"SIGGRAPH",
7+
"ICPC",
8+
"SIGMobile",
9+
"SIGMusic",
10+
"GLUG",
11+
"SIGNLL",
12+
"SIGma",
13+
"SIGQuantum",
14+
"SIGecom",
15+
"SIGPLAN",
16+
"SIGPolicy",
17+
"SIGARCH",
18+
] as const;
19+
20+
export const CommitteeList = [
21+
"Infrastructure Committe",
22+
"Social Committee",
23+
"Mentorship Committee",
24+
] as const;
25+
export const OrganizationList = ["ACM", ...SIGList, ...CommitteeList] as const;

src/plugins/errorHandler.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
import { FastifyReply, FastifyRequest } from "fastify";
1+
import { FastifyError, FastifyReply, FastifyRequest } from "fastify";
22
import fp from "fastify-plugin";
33
import {
44
BaseError,
55
InternalServerError,
66
NotFoundError,
7+
ValidationError,
78
} from "../errors/index.js";
89

910
const errorHandlerPlugin = fp(async (fastify) => {
1011
fastify.setErrorHandler(
1112
(err: unknown, request: FastifyRequest, reply: FastifyReply) => {
12-
let finalErr = new InternalServerError();
13+
let finalErr;
1314
if (err instanceof BaseError) {
1415
finalErr = err;
16+
} else if (
17+
(err as FastifyError).validation ||
18+
(err as Error).name === "BadRequestError"
19+
) {
20+
finalErr = new ValidationError({
21+
message: (err as FastifyError).message,
22+
});
1523
}
16-
if (err instanceof BaseError) {
24+
if (finalErr && finalErr instanceof BaseError) {
1725
request.log.error(
18-
{ errId: err.id, errName: err.name },
26+
{ errId: finalErr.id, errName: finalErr.name },
1927
finalErr.toString(),
2028
);
2129
} else if (err instanceof Error) {
@@ -26,6 +34,9 @@ const errorHandlerPlugin = fp(async (fastify) => {
2634
} else {
2735
request.log.error("Native unhandled error: response sent to client.");
2836
}
37+
if (!finalErr) {
38+
finalErr = new InternalServerError();
39+
}
2940
reply.status(finalErr.httpStatusCode).type("application/json").send({
3041
error: true,
3142
name: finalErr.name,

src/routes/event.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { FastifyPluginAsync } from "fastify";
2+
import { AppRoles } from "../roles.js";
3+
import { z } from "zod";
4+
import { zodToJsonSchema } from "zod-to-json-schema";
5+
import { OrganizationList } from "../orgs.js";
6+
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
7+
import { marshall } from "@aws-sdk/util-dynamodb";
8+
import config from "../config.js";
9+
import { DatabaseInsertError } from "../errors/index.js";
10+
import { randomUUID } from "crypto";
11+
12+
const repeatOptions = ["weekly", "biweekly"] as const;
13+
14+
const requestBodySchema = z.object({
15+
title: z.string().min(1),
16+
description: z.string().min(1),
17+
start: z.string().datetime(),
18+
end: z.optional(z.string().datetime()),
19+
location: z.string(),
20+
locationLink: z.optional(z.string().url()),
21+
repeats: z.optional(z.enum(repeatOptions)),
22+
host: z.enum(OrganizationList),
23+
});
24+
const requestJsonSchema = zodToJsonSchema(requestBodySchema);
25+
type EventPostRequest = z.infer<typeof requestBodySchema>;
26+
27+
const responseJsonSchema = zodToJsonSchema(
28+
z.object({
29+
id: z.string(),
30+
resource: z.string(),
31+
}),
32+
);
33+
34+
const dynamoClient = new DynamoDBClient({
35+
region: process.env.AWS_REGION || "us-east-1",
36+
});
37+
38+
const createEvent: FastifyPluginAsync = async (fastify, _options) => {
39+
fastify.post<{ Body: EventPostRequest }>(
40+
"/",
41+
{
42+
schema: {
43+
body: requestJsonSchema,
44+
response: { 200: responseJsonSchema },
45+
},
46+
onRequest: async (request, reply) => {
47+
await fastify.authorize(request, reply, [AppRoles.MANAGER]);
48+
},
49+
},
50+
async (request, reply) => {
51+
try {
52+
const entryUUID = randomUUID().toString();
53+
const dynamoResponse = await dynamoClient.send(
54+
new PutItemCommand({
55+
TableName: config.DYNAMO_TABLE_NAME,
56+
Item: marshall({ ...request.body, id: entryUUID }),
57+
}),
58+
);
59+
reply.send({
60+
id: entryUUID,
61+
resource: `/api/v1/entry/${entryUUID}`,
62+
});
63+
} catch (e: unknown) {
64+
if (e instanceof Error) {
65+
request.log.error("Failed to insert to DynamoDB: " + e.toString());
66+
}
67+
throw new DatabaseInsertError({
68+
message: "Failed to insert event to Dynamo table.",
69+
});
70+
}
71+
},
72+
);
73+
};
74+
75+
export default createEvent;

0 commit comments

Comments
 (0)