Skip to content

Commit 72b5291

Browse files
authored
Merge branch 'main' into siglead-management
2 parents aa6a9d3 + aaf959e commit 72b5291

File tree

6 files changed

+177
-48
lines changed

6 files changed

+177
-48
lines changed

src/api/functions/membership.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { EntraGroupError } from "common/errors/index.js";
1616
import { EntraGroupActions } from "common/types/iam.js";
1717
import { pollUntilNoError } from "./general.js";
1818

19+
export const MEMBER_CACHE_SECONDS = 43200; // 12 hours
20+
1921
export async function checkExternalMembership(
2022
netId: string,
2123
list: string,

src/api/routes/membership.ts

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
checkPaidMembershipFromEntra,
44
checkPaidMembershipFromTable,
55
setPaidMembershipInTable,
6+
MEMBER_CACHE_SECONDS,
67
} from "api/functions/membership.js";
78
import { validateNetId } from "api/functions/validation.js";
89
import { FastifyPluginAsync } from "fastify";
@@ -26,9 +27,7 @@ import rawbody from "fastify-raw-body";
2627
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
2728
import { z } from "zod";
2829
import { withTags } from "api/components/index.js";
29-
30-
const NONMEMBER_CACHE_SECONDS = 60; // 1 minute
31-
const MEMBER_CACHE_SECONDS = 43200; // 12 hours
30+
import { getKey, setKey } from "api/functions/redisCache.js";
3231

3332
const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
3433
await fastify.register(rawbody, {
@@ -89,7 +88,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
8988
},
9089
async (request, reply) => {
9190
const netId = request.params.netId.toLowerCase();
92-
if (fastify.nodeCache.get(`isMember_${netId}`) === true) {
91+
const cacheKey = `membership:${netId}:acmpaid`;
92+
const result = await getKey<{ isMember: boolean }>({
93+
redisClient: fastify.redisClient,
94+
key: cacheKey,
95+
logger: request.log,
96+
});
97+
if (result && result.isMember) {
9398
throw new ValidationError({
9499
message: `${netId} is already a paid member!`,
95100
});
@@ -99,11 +104,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
99104
fastify.dynamoClient,
100105
);
101106
if (isDynamoMember) {
102-
fastify.nodeCache.set(
103-
`isMember_${netId}`,
104-
true,
105-
MEMBER_CACHE_SECONDS,
106-
);
107+
await setKey({
108+
redisClient: fastify.redisClient,
109+
key: cacheKey,
110+
data: JSON.stringify({ isMember: true }),
111+
expiresIn: MEMBER_CACHE_SECONDS,
112+
logger: request.log,
113+
});
107114
throw new ValidationError({
108115
message: `${netId} is already a paid member!`,
109116
});
@@ -121,11 +128,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
121128
paidMemberGroup,
122129
);
123130
if (isAadMember) {
124-
fastify.nodeCache.set(
125-
`isMember_${netId}`,
126-
true,
127-
MEMBER_CACHE_SECONDS,
128-
);
131+
await setKey({
132+
redisClient: fastify.redisClient,
133+
key: cacheKey,
134+
data: JSON.stringify({ isMember: true }),
135+
expiresIn: MEMBER_CACHE_SECONDS,
136+
logger: request.log,
137+
});
129138
reply
130139
.header("X-ACM-Data-Source", "aad")
131140
.send({ netId, isPaidMember: true });
@@ -134,11 +143,14 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
134143
message: `${netId} is already a paid member!`,
135144
});
136145
}
137-
fastify.nodeCache.set(
138-
`isMember_${netId}`,
139-
false,
140-
NONMEMBER_CACHE_SECONDS,
141-
);
146+
// Once the caller becomes a member, the stripe webhook will handle changing this to true
147+
await setKey({
148+
redisClient: fastify.redisClient,
149+
key: cacheKey,
150+
data: JSON.stringify({ isMember: false }),
151+
expiresIn: MEMBER_CACHE_SECONDS,
152+
logger: request.log,
153+
});
142154
const secretApiConfig =
143155
(await getSecretValue(
144156
fastify.secretsManagerClient,
@@ -190,11 +202,19 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
190202
async (request, reply) => {
191203
const netId = request.params.netId.toLowerCase();
192204
const list = request.query.list || "acmpaid";
193-
if (fastify.nodeCache.get(`isMember_${netId}_${list}`) !== undefined) {
205+
// we don't control external list as its direct upload in Dynamo, cache only for 60 seconds.
206+
const ourCacheSeconds = list === "acmpaid" ? MEMBER_CACHE_SECONDS : 60;
207+
const cacheKey = `membership:${netId}:${list}`;
208+
const result = await getKey<{ isMember: boolean }>({
209+
redisClient: fastify.redisClient,
210+
key: cacheKey,
211+
logger: request.log,
212+
});
213+
if (result) {
194214
return reply.header("X-ACM-Data-Source", "cache").send({
195215
netId,
196216
list: list === "acmpaid" ? undefined : list,
197-
isPaidMember: fastify.nodeCache.get(`isMember_${netId}_${list}`),
217+
isPaidMember: result.isMember,
198218
});
199219
}
200220
if (list !== "acmpaid") {
@@ -203,11 +223,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
203223
list,
204224
fastify.dynamoClient,
205225
);
206-
fastify.nodeCache.set(
207-
`isMember_${netId}_${list}`,
208-
isMember,
209-
MEMBER_CACHE_SECONDS,
210-
);
226+
await setKey({
227+
redisClient: fastify.redisClient,
228+
key: cacheKey,
229+
data: JSON.stringify({ isMember }),
230+
expiresIn: ourCacheSeconds,
231+
logger: request.log,
232+
});
211233
return reply.header("X-ACM-Data-Source", "dynamo").send({
212234
netId,
213235
list,
@@ -219,11 +241,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
219241
fastify.dynamoClient,
220242
);
221243
if (isDynamoMember) {
222-
fastify.nodeCache.set(
223-
`isMember_${netId}_${list}`,
224-
true,
225-
MEMBER_CACHE_SECONDS,
226-
);
244+
await setKey({
245+
redisClient: fastify.redisClient,
246+
key: cacheKey,
247+
data: JSON.stringify({ isMember: true }),
248+
expiresIn: ourCacheSeconds,
249+
logger: request.log,
250+
});
227251
return reply
228252
.header("X-ACM-Data-Source", "dynamo")
229253
.send({ netId, isPaidMember: true });
@@ -241,22 +265,26 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
241265
paidMemberGroup,
242266
);
243267
if (isAadMember) {
244-
fastify.nodeCache.set(
245-
`isMember_${netId}_${list}`,
246-
true,
247-
MEMBER_CACHE_SECONDS,
248-
);
268+
await setKey({
269+
redisClient: fastify.redisClient,
270+
key: cacheKey,
271+
data: JSON.stringify({ isMember: true }),
272+
expiresIn: ourCacheSeconds,
273+
logger: request.log,
274+
});
249275
reply
250276
.header("X-ACM-Data-Source", "aad")
251277
.send({ netId, isPaidMember: true });
252278
await setPaidMembershipInTable(netId, fastify.dynamoClient);
253279
return;
254280
}
255-
fastify.nodeCache.set(
256-
`isMember_${netId}_${list}`,
257-
false,
258-
NONMEMBER_CACHE_SECONDS,
259-
);
281+
await setKey({
282+
redisClient: fastify.redisClient,
283+
key: cacheKey,
284+
data: JSON.stringify({ isMember: false }),
285+
expiresIn: ourCacheSeconds,
286+
logger: request.log,
287+
});
260288
return reply
261289
.header("X-ACM-Data-Source", "aad")
262290
.send({ netId, isPaidMember: false });
@@ -315,6 +343,7 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
315343
) {
316344
const customerEmail = event.data.object.customer_email;
317345
if (!customerEmail) {
346+
request.log.info("No customer email found.");
318347
return reply
319348
.code(200)
320349
.send({ handled: false, requestId: request.id });

src/api/routes/roomRequests.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
3030
import { z } from "zod";
3131
import { buildAuditLogTransactPut } from "api/functions/auditLog.js";
3232
import { Modules } from "common/modules.js";
33+
import {
34+
generateProjectionParams,
35+
getDefaultFilteringQuerystring,
36+
nonEmptyCommaSeparatedStringSchema,
37+
} from "common/utils.js";
3338

3439
const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
3540
await fastify.register(rateLimiter, {
@@ -182,12 +187,19 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
182187
example: "sp25",
183188
}),
184189
}),
190+
querystring: z.object(
191+
getDefaultFilteringQuerystring({
192+
defaultSelect: ["requestId", "title"],
193+
}),
194+
),
185195
}),
186196
),
187197
onRequest: fastify.authorizeFromSchema,
188198
},
189199
async (request, reply) => {
190200
const semesterId = request.params.semesterId;
201+
const { ProjectionExpression, ExpressionAttributeNames } =
202+
generateProjectionParams({ userFields: request.query.select });
191203
if (!request.username) {
192204
throw new InternalServerError({
193205
message: "Could not retrieve username.",
@@ -198,6 +210,8 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
198210
command = new QueryCommand({
199211
TableName: genericConfig.RoomRequestsTableName,
200212
KeyConditionExpression: "semesterId = :semesterValue",
213+
ProjectionExpression,
214+
ExpressionAttributeNames,
201215
ExpressionAttributeValues: {
202216
":semesterValue": { S: semesterId },
203217
},
@@ -209,8 +223,9 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
209223
"semesterId = :semesterValue AND begins_with(#sortKey, :username)",
210224
ExpressionAttributeNames: {
211225
"#sortKey": "userId#requestId",
226+
...ExpressionAttributeNames,
212227
},
213-
ProjectionExpression: "requestId, host, title, semester",
228+
ProjectionExpression,
214229
ExpressionAttributeValues: {
215230
":semesterValue": { S: semesterId },
216231
":username": { S: request.username },
@@ -224,6 +239,9 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
224239
});
225240
}
226241
const items = response.Items.map((x) => {
242+
if (!request.query.select.includes("status")) {
243+
return unmarshall(x);
244+
}
227245
const item = unmarshall(x) as {
228246
host: string;
229247
title: string;
@@ -403,20 +421,29 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
403421
example: "sp25",
404422
}),
405423
}),
424+
querystring: z.object(
425+
getDefaultFilteringQuerystring({
426+
defaultSelect: ["requestId", "title"],
427+
}),
428+
),
406429
}),
407430
),
408431
onRequest: fastify.authorizeFromSchema,
409432
},
410433
async (request, reply) => {
411434
const requestId = request.params.requestId;
412435
const semesterId = request.params.semesterId;
436+
const { ProjectionExpression, ExpressionAttributeNames } =
437+
generateProjectionParams({ userFields: request.query.select });
413438
let command;
414439
if (request.userRoles?.has(AppRoles.BYPASS_OBJECT_LEVEL_AUTH)) {
415440
command = new QueryCommand({
416441
TableName: genericConfig.RoomRequestsTableName,
417442
IndexName: "RequestIdIndex",
418443
KeyConditionExpression: "requestId = :requestId",
419444
FilterExpression: "semesterId = :semesterId",
445+
ProjectionExpression,
446+
ExpressionAttributeNames,
420447
ExpressionAttributeValues: {
421448
":requestId": { S: requestId },
422449
":semesterId": { S: semesterId },
@@ -426,6 +453,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
426453
} else {
427454
command = new QueryCommand({
428455
TableName: genericConfig.RoomRequestsTableName,
456+
ProjectionExpression,
429457
KeyConditionExpression:
430458
"semesterId = :semesterId AND #userIdRequestId = :userRequestId",
431459
ExpressionAttributeValues: {
@@ -434,6 +462,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
434462
},
435463
ExpressionAttributeNames: {
436464
"#userIdRequestId": "userId#requestId",
465+
...ExpressionAttributeNames,
437466
},
438467
Limit: 1,
439468
});

src/api/sqs/handlers/provisionNewMember.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { AvailableSQSFunctions } from "common/types/sqsMessage.js";
22
import { currentEnvironmentConfig, SQSHandlerFunction } from "../index.js";
33
import { getEntraIdToken } from "../../../api/functions/entraId.js";
4-
import { genericConfig } from "../../../common/config.js";
4+
import { genericConfig, SecretConfig } from "../../../common/config.js";
55

6-
import { setPaidMembership } from "api/functions/membership.js";
6+
import {
7+
MEMBER_CACHE_SECONDS,
8+
setPaidMembership,
9+
} from "api/functions/membership.js";
710
import { createAuditLogEntry } from "api/functions/auditLog.js";
811
import { Modules } from "common/modules.js";
9-
import { getAuthorizedClients } from "../utils.js";
12+
import { getAuthorizedClients, getSecretConfig } from "../utils.js";
1013
import { emailMembershipPassHandler } from "./emailMembershipPassHandler.js";
14+
import RedisModule from "ioredis";
15+
import { setKey } from "api/functions/redisCache.js";
1116

1217
export const provisionNewMemberHandler: SQSHandlerFunction<
1318
AvailableSQSFunctions.ProvisionNewMember
@@ -21,9 +26,16 @@ export const provisionNewMemberHandler: SQSHandlerFunction<
2126
secretName: genericConfig.EntraSecretName,
2227
logger,
2328
});
29+
const secretConfig: SecretConfig = await getSecretConfig({
30+
logger,
31+
commonConfig,
32+
});
33+
const redisClient = new RedisModule.default(secretConfig.redis_url);
34+
const netId = email.replace("@illinois.edu", "");
35+
const cacheKey = `membership:${netId}:acmpaid`;
2436
logger.info("Got authorized clients and Entra ID token.");
2537
const { updated } = await setPaidMembership({
26-
netId: email.replace("@illinois.edu", ""),
38+
netId,
2739
dynamoClient: clients.dynamoClient,
2840
entraToken,
2941
paidMemberGroup: currentEnvironmentConfig.PaidMemberGroupId,
@@ -45,4 +57,12 @@ export const provisionNewMemberHandler: SQSHandlerFunction<
4557
} else {
4658
logger.info(`${email} was already a paid member.`);
4759
}
60+
logger.info("Setting membership in Redis.");
61+
await setKey({
62+
redisClient,
63+
key: cacheKey,
64+
data: JSON.stringify({ isMember: true }),
65+
expiresIn: MEMBER_CACHE_SECONDS,
66+
logger,
67+
});
4868
};

0 commit comments

Comments
 (0)