Skip to content

Commit 86a2126

Browse files
committed
POC code for RSVPs
1 parent 24f08e3 commit 86a2126

File tree

3 files changed

+107
-0
lines changed

3 files changed

+107
-0
lines changed

src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { docsHtml, securitySchemes } from "./docs.js";
6262
import syncIdentityPlugin from "./routes/syncIdentity.js";
6363
import { createRedisModule } from "./redis.js";
6464
import userRoute from "./routes/user.js";
65+
import rsvpRoutes from "./routes/rsvp.js";
6566
/** END ROUTES */
6667

6768
export const instanceId = randomUUID();
@@ -377,6 +378,7 @@ Otherwise, email [infra@acm.illinois.edu](mailto:infra@acm.illinois.edu) for sup
377378
api.register(apiKeyRoute, { prefix: "/apiKey" });
378379
api.register(clearSessionRoute, { prefix: "/clearSession" });
379380
api.register(userRoute, { prefix: "/users" });
381+
api.register(rsvpRoutes, { prefix: "/rsvp" });
380382
if (app.runEnvironment === "dev") {
381383
api.register(vendingPlugin, { prefix: "/vending" });
382384
}

src/api/routes/rsvp.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { FastifyPluginAsync } from "fastify";
2+
import rateLimiter from "api/plugins/rateLimiter.js";
3+
import { withRoles, withTags } from "api/components/index.js";
4+
import { getUserOrgRoles } from "api/functions/organizations.js";
5+
import {
6+
UnauthenticatedError,
7+
UnauthorizedError,
8+
ValidationError,
9+
} from "common/errors/index.js";
10+
import * as z from "zod/v4";
11+
import { verifyUiucAccessToken } from "api/functions/uin.js";
12+
import { checkPaidMembership } from "api/functions/membership.js";
13+
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
14+
15+
const rsvpRoutes: FastifyPluginAsync = async (fastify, _options) => {
16+
await fastify.register(rateLimiter, {
17+
limit: 30,
18+
duration: 30,
19+
rateLimitIdentifier: "rsvp",
20+
});
21+
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post(
22+
"/:orgId/event/:eventId",
23+
{
24+
schema: withTags(["RSVP"], {
25+
summary: "Submit an RSVP for an event.",
26+
params: z.object({
27+
eventId: z.string().min(1).meta({
28+
description: "The previously-created event ID in the events API.",
29+
}),
30+
}),
31+
headers: z.object({
32+
"x-uiuc-token": z.jwt().min(1).meta({
33+
description:
34+
"An access token for the user in the UIUC Entra ID tenant.",
35+
}),
36+
}),
37+
}),
38+
},
39+
async (request, reply) => {
40+
const accessToken = request.headers["x-uiuc-token"];
41+
const verifiedData = await verifyUiucAccessToken({
42+
accessToken,
43+
logger: request.log,
44+
});
45+
const { userPrincipalName: upn, givenName, surname } = verifiedData;
46+
const netId = upn.replace("@illinois.edu", "");
47+
if (netId.includes("@")) {
48+
request.log.error(
49+
`Found UPN ${upn} which cannot be turned into NetID via simple replacement.`,
50+
);
51+
throw new ValidationError({
52+
message: "ID token could not be parsed.",
53+
});
54+
}
55+
const isPaidMember = await checkPaidMembership({
56+
netId,
57+
dynamoClient: fastify.dynamoClient,
58+
redisClient: fastify.redisClient,
59+
logger: request.log,
60+
});
61+
const entry = {
62+
partitionKey: `${request.params.eventId}#${upn}`,
63+
eventId: request.params.eventId,
64+
userId: upn,
65+
isPaidMember,
66+
createdAt: "",
67+
};
68+
},
69+
);
70+
};
71+
72+
export default rsvpRoutes;

terraform/modules/dynamo/main.tf

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,36 @@ resource "aws_dynamodb_table" "store_limits" {
567567
}
568568
}
569569
}
570+
571+
resource "aws_dynamodb_table" "store_limits" {
572+
region = "us-east-2"
573+
billing_mode = "PAY_PER_REQUEST"
574+
name = "${var.ProjectId}-events-rsvp"
575+
deletion_protection_enabled = true
576+
hash_key = "partitionKey"
577+
point_in_time_recovery {
578+
enabled = true
579+
}
580+
attribute {
581+
name = "partitionKey"
582+
type = "S"
583+
}
584+
attribute {
585+
name = "eventId"
586+
type = "S"
587+
}
588+
global_secondary_index {
589+
name = "EventIdIndex"
590+
hash_key = "eventId"
591+
projection_type = "ALL"
592+
}
593+
stream_enabled = true
594+
stream_view_type = "NEW_AND_OLD_IMAGES"
595+
dynamic "replica" {
596+
for_each = var.ReplicationRegions
597+
content {
598+
region_name = replica.value
599+
deletion_protection_enabled = true
600+
}
601+
}
602+
}

0 commit comments

Comments
 (0)