Skip to content

Commit 54fb945

Browse files
committed
basic room requests setup
1 parent eb765ee commit 54fb945

File tree

12 files changed

+758
-48
lines changed

12 files changed

+758
-48
lines changed

cloudformation/iam.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Resources:
7272
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links
7373
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-membership-provisioning
7474
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-membership-external
75+
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-room-requests
7576

7677
- Sid: DynamoDBCacheAccess
7778
Effect: Allow
@@ -102,6 +103,7 @@ Resources:
102103
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links/index/*
103104
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-events/index/*
104105
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-purchase-history/index/*
106+
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-room-requests/index/*
105107

106108
- Sid: DynamoDBStreamAccess
107109
Effect: Allow

cloudformation/main.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,23 @@ Resources:
307307
- AttributeName: netid_list
308308
KeyType: HASH
309309

310+
RoomRequestsTable:
311+
Type: "AWS::DynamoDB::Table"
312+
DeletionPolicy: "Retain"
313+
UpdateReplacePolicy: "Retain"
314+
Properties:
315+
BillingMode: "PAY_PER_REQUEST"
316+
TableName: infra-core-api-room-requests
317+
DeletionProtectionEnabled: true
318+
PointInTimeRecoverySpecification:
319+
PointInTimeRecoveryEnabled: !If [IsProd, true, false]
320+
AttributeDefinitions:
321+
- AttributeName: id
322+
AttributeType: S
323+
KeySchema:
324+
- AttributeName: id
325+
KeyType: HASH
326+
310327
IamGroupRolesTable:
311328
Type: "AWS::DynamoDB::Table"
312329
DeletionPolicy: "Retain"

src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import mobileWalletRoute from "./routes/mobileWallet.js";
2626
import stripeRoutes from "./routes/stripe.js";
2727
import membershipPlugin from "./routes/membership.js";
2828
import path from "path"; // eslint-disable-line import/no-nodejs-modules
29+
import roomRequestRoutes from "./routes/roomRequests.js";
2930

3031
dotenv.config();
3132

@@ -133,6 +134,7 @@ async function init(prettyPrint: boolean = false) {
133134
api.register(ticketsPlugin, { prefix: "/tickets" });
134135
api.register(mobileWalletRoute, { prefix: "/mobileWallet" });
135136
api.register(stripeRoutes, { prefix: "/stripe" });
137+
api.register(roomRequestRoutes, { prefix: "/roomRequests" });
136138
if (app.runEnvironment === "dev") {
137139
api.register(vendingPlugin, { prefix: "/vending" });
138140
}

src/api/routes/roomRequests.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { FastifyPluginAsync } from "fastify";
2+
import rateLimiter from "api/plugins/rateLimiter.js";
3+
import {
4+
RoomRequestFormValues,
5+
roomRequestPostResponse,
6+
roomRequestSchema,
7+
RoomRequestStatus,
8+
} from "common/types/roomRequest.js";
9+
import { AppRoles } from "common/roles.js";
10+
import { zodToJsonSchema } from "zod-to-json-schema";
11+
import { randomUUID } from "crypto";
12+
13+
const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
14+
await fastify.register(rateLimiter, {
15+
limit: 5,
16+
duration: 30,
17+
rateLimitIdentifier: "roomRequests",
18+
});
19+
fastify.post<{ Body: RoomRequestFormValues }>(
20+
"/",
21+
{
22+
schema: {
23+
response: { 201: zodToJsonSchema(roomRequestPostResponse) },
24+
},
25+
preValidation: async (request, reply) => {
26+
await fastify.zodValidateBody(request, reply, roomRequestSchema);
27+
},
28+
onRequest: async (request, reply) => {
29+
await fastify.authorize(request, reply, [AppRoles.ROOM_REQUEST_CREATE]);
30+
},
31+
},
32+
async (request, reply) => {
33+
const id = randomUUID().toString();
34+
reply.status(201).send({
35+
id,
36+
status: RoomRequestStatus.CREATED,
37+
});
38+
},
39+
);
40+
};
41+
42+
export default roomRequestRoutes;

src/common/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type GenericConfigType = {
3939
MerchStoreMetadataTableName: string;
4040
IAMTablePrefix: string;
4141
ProtectedEntraIDGroups: string[]; // these groups are too privileged to be modified via this portal and must be modified directly in Entra ID.
42+
RoomRequestsTableName: string;
4243
};
4344

4445
type EnvironmentConfigType = {
@@ -71,7 +72,8 @@ const genericConfig: GenericConfigType = {
7172
IAMTablePrefix: "infra-core-api-iam",
7273
ProtectedEntraIDGroups: [infraChairsGroupId, officersGroupId],
7374
MembershipTableName: "infra-core-api-membership-provisioning",
74-
ExternalMembershipTableName: "infra-core-api-membership-external"
75+
ExternalMembershipTableName: "infra-core-api-membership-external",
76+
RoomRequestsTableName: "infra-core-api-room-requests"
7577
} as const;
7678

7779
const environmentConfig: EnvironmentConfigType = {

src/common/types/roomRequest.ts

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,67 @@
1-
import { z } from "zod";
2-
import { OrganizationList } from "../orgs";
3-
4-
export const roomRequestPostSchema = z.object({
5-
organization: z.enum(OrganizationList),
6-
name: z.string().min(1),
7-
description: z.string().min(1),
8-
start: z.number().min(0),
9-
end: z.number().min(0).max(604800),
1+
import { z } from 'zod';
2+
import { OrganizationList } from '../orgs.js';
3+
4+
export const eventThemeOptions = [
5+
"Arts & Music",
6+
"Athletics",
7+
"Cultural",
8+
"Fundraising",
9+
"Group Business",
10+
"Learning",
11+
"Service",
12+
"Social",
13+
"Spirituality"
14+
] as [string, ...string[]];
15+
16+
export const spaceTypeOptions = [
17+
{ value: "campus_classroom", label: "Campus Classroom" },
18+
{ value: "campus_performance", label: "Campus Performance Space *" },
19+
{ value: "bif", label: "Business Instructional Facility (BIF)" },
20+
{ value: "campus_rec", label: "Campus Rec (ARC, CRCE, Ice Arena, Illini Grove) *" },
21+
{ value: "illini_union", label: "Illini Union *" },
22+
{ value: "stock_pavilion", label: "Stock Pavilion" }
23+
];
24+
25+
26+
export enum RoomRequestStatus {
27+
CREATED = "created",
28+
REJECTED_BY_ACM = "rejected_by_acm",
29+
SUBMITTED = "submitted",
30+
APPROVED = "approved",
31+
REJECTED_BY_UIUC = "rejected_by_uiuc"
32+
}
33+
34+
export const roomRequestSchema = z.object({
35+
host: z.enum(OrganizationList),
36+
title: z.string().min(2, "Title must have at least 2 characters"),
37+
theme: z.enum(eventThemeOptions),
38+
description: z.string()
39+
.min(10, "Description must have at least 10 words")
40+
.max(1000, "Description cannot exceed 1000 characters")
41+
.refine(val => val.split(/\s+/).filter(Boolean).length >= 10, {
42+
message: "Description must have at least 10 words"
43+
}),
44+
hostingMinors: z.boolean().nullable().optional(),
45+
locationType: z.enum(['in-person', 'virtual', 'both']),
46+
spaceType: z.string().optional(),
47+
specificRoom: z.string().optional(),
48+
estimatedAttendees: z.number().positive().optional(),
49+
seatsNeeded: z.number().positive().optional(),
50+
setupDetails: z.string().nullable().optional(),
51+
onCampusPartners: z.string().nullable().optional(),
52+
offCampusPartners: z.string().nullable().optional(),
53+
nonIllinoisSpeaker: z.string().nullable().optional(),
54+
nonIllinoisAttendees: z.number().nullable().optional(),
55+
foodOrDrink: z.boolean().nullable().optional(),
56+
crafting: z.boolean().nullable().optional(),
57+
comments: z.string().optional(),
1058
});
1159

12-
export type RoomRequestPostRequest = z.infer<typeof roomRequestPostSchema>;
60+
export type RoomRequestFormValues = z.infer<typeof roomRequestSchema>;
61+
62+
export const roomRequestPostResponse = z.object({
63+
id: z.string().uuid(),
64+
status: z.literal(RoomRequestStatus.CREATED),
65+
})
66+
67+
export type RoomRequestPostResponse = z.infer<typeof roomRequestPostResponse>;

src/ui/Router.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ViewTicketsPage } from './pages/tickets/ViewTickets.page';
1919
import { ManageIamPage } from './pages/iam/ManageIam.page';
2020
import { ManageProfilePage } from './pages/profile/ManageProfile.page';
2121
import { ManageStripeLinksPage } from './pages/stripe/ViewLinks.page';
22-
import { ManageRoomRequestsPage } from './pages/roomRequest/RoomRequest.page';
22+
import { ManageRoomRequestsPage } from './pages/roomRequest/RoomRequestLanding.page';
2323

2424
const ProfileRediect: React.FC = () => {
2525
const location = useLocation();
@@ -164,7 +164,7 @@ const authenticatedRouter = createBrowserRouter([
164164
element: <ManageStripeLinksPage />,
165165
},
166166
{
167-
path: '/roomRequest',
167+
path: '/roomRequests',
168168
element: <ManageRoomRequestsPage />,
169169
},
170170
// Catch-all route for authenticated users shows 404 page

src/ui/components/AppShell/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const navItems = [
6767
validRoles: [AppRoles.STRIPE_LINK_CREATOR],
6868
},
6969
{
70-
link: '/roomRequest',
70+
link: '/roomRequests',
7171
name: 'Room Requests',
7272
icon: IconDoor,
7373
description: null,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const ExistingRoomRequests: React.FC = () => {
2+
return null;
3+
};
4+
5+
export default ExistingRoomRequests;

0 commit comments

Comments
 (0)