Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/api/functions/membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { EntraGroupError } from "common/errors/index.js";
import { EntraGroupActions } from "common/types/iam.js";
import { pollUntilNoError } from "./general.js";

export const MEMBER_CACHE_SECONDS = 43200; // 12 hours

export async function checkExternalMembership(
netId: string,
list: string,
Expand Down
111 changes: 70 additions & 41 deletions src/api/routes/membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
checkPaidMembershipFromEntra,
checkPaidMembershipFromTable,
setPaidMembershipInTable,
MEMBER_CACHE_SECONDS,
} from "api/functions/membership.js";
import { validateNetId } from "api/functions/validation.js";
import { FastifyPluginAsync } from "fastify";
Expand All @@ -26,9 +27,7 @@ import rawbody from "fastify-raw-body";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { z } from "zod";
import { withTags } from "api/components/index.js";

const NONMEMBER_CACHE_SECONDS = 60; // 1 minute
const MEMBER_CACHE_SECONDS = 43200; // 12 hours
import { getKey, setKey } from "api/functions/redisCache.js";

const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
await fastify.register(rawbody, {
Expand Down Expand Up @@ -89,7 +88,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
},
async (request, reply) => {
const netId = request.params.netId.toLowerCase();
if (fastify.nodeCache.get(`isMember_${netId}`) === true) {
const cacheKey = `membership:${netId}:acmpaid`;
const result = await getKey<{ isMember: boolean }>({
redisClient: fastify.redisClient,
key: cacheKey,
logger: request.log,
});
if (result && result.isMember) {
throw new ValidationError({
message: `${netId} is already a paid member!`,
});
Expand All @@ -99,11 +104,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
fastify.dynamoClient,
);
if (isDynamoMember) {
fastify.nodeCache.set(
`isMember_${netId}`,
true,
MEMBER_CACHE_SECONDS,
);
await setKey({
redisClient: fastify.redisClient,
key: cacheKey,
data: JSON.stringify({ isMember: true }),
expiresIn: MEMBER_CACHE_SECONDS,
logger: request.log,
});
throw new ValidationError({
message: `${netId} is already a paid member!`,
});
Expand All @@ -121,11 +128,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
paidMemberGroup,
);
if (isAadMember) {
fastify.nodeCache.set(
`isMember_${netId}`,
true,
MEMBER_CACHE_SECONDS,
);
await setKey({
redisClient: fastify.redisClient,
key: cacheKey,
data: JSON.stringify({ isMember: true }),
expiresIn: MEMBER_CACHE_SECONDS,
logger: request.log,
});
reply
.header("X-ACM-Data-Source", "aad")
.send({ netId, isPaidMember: true });
Expand All @@ -134,11 +143,14 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
message: `${netId} is already a paid member!`,
});
}
fastify.nodeCache.set(
`isMember_${netId}`,
false,
NONMEMBER_CACHE_SECONDS,
);
// Once the caller becomes a member, the stripe webhook will handle changing this to true
await setKey({
redisClient: fastify.redisClient,
key: cacheKey,
data: JSON.stringify({ isMember: false }),
expiresIn: MEMBER_CACHE_SECONDS,
logger: request.log,
});
const secretApiConfig =
(await getSecretValue(
fastify.secretsManagerClient,
Expand Down Expand Up @@ -190,11 +202,19 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
async (request, reply) => {
const netId = request.params.netId.toLowerCase();
const list = request.query.list || "acmpaid";
if (fastify.nodeCache.get(`isMember_${netId}_${list}`) !== undefined) {
// we don't control external list as its direct upload in Dynamo, cache only for 60 seconds.
const ourCacheSeconds = list === "acmpaid" ? MEMBER_CACHE_SECONDS : 60;
const cacheKey = `membership:${netId}:${list}`;
const result = await getKey<{ isMember: boolean }>({
redisClient: fastify.redisClient,
key: cacheKey,
logger: request.log,
});
if (result) {
return reply.header("X-ACM-Data-Source", "cache").send({
netId,
list: list === "acmpaid" ? undefined : list,
isPaidMember: fastify.nodeCache.get(`isMember_${netId}_${list}`),
isPaidMember: result.isMember,
});
}
if (list !== "acmpaid") {
Expand All @@ -203,11 +223,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
list,
fastify.dynamoClient,
);
fastify.nodeCache.set(
`isMember_${netId}_${list}`,
isMember,
MEMBER_CACHE_SECONDS,
);
await setKey({
redisClient: fastify.redisClient,
key: cacheKey,
data: JSON.stringify({ isMember }),
expiresIn: ourCacheSeconds,
logger: request.log,
});
return reply.header("X-ACM-Data-Source", "dynamo").send({
netId,
list,
Expand All @@ -219,11 +241,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
fastify.dynamoClient,
);
if (isDynamoMember) {
fastify.nodeCache.set(
`isMember_${netId}_${list}`,
true,
MEMBER_CACHE_SECONDS,
);
await setKey({
redisClient: fastify.redisClient,
key: cacheKey,
data: JSON.stringify({ isMember: true }),
expiresIn: ourCacheSeconds,
logger: request.log,
});
return reply
.header("X-ACM-Data-Source", "dynamo")
.send({ netId, isPaidMember: true });
Expand All @@ -241,22 +265,26 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
paidMemberGroup,
);
if (isAadMember) {
fastify.nodeCache.set(
`isMember_${netId}_${list}`,
true,
MEMBER_CACHE_SECONDS,
);
await setKey({
redisClient: fastify.redisClient,
key: cacheKey,
data: JSON.stringify({ isMember: true }),
expiresIn: ourCacheSeconds,
logger: request.log,
});
reply
.header("X-ACM-Data-Source", "aad")
.send({ netId, isPaidMember: true });
await setPaidMembershipInTable(netId, fastify.dynamoClient);
return;
}
fastify.nodeCache.set(
`isMember_${netId}_${list}`,
false,
NONMEMBER_CACHE_SECONDS,
);
await setKey({
redisClient: fastify.redisClient,
key: cacheKey,
data: JSON.stringify({ isMember: false }),
expiresIn: ourCacheSeconds,
logger: request.log,
});
return reply
.header("X-ACM-Data-Source", "aad")
.send({ netId, isPaidMember: false });
Expand Down Expand Up @@ -315,6 +343,7 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
) {
const customerEmail = event.data.object.customer_email;
if (!customerEmail) {
request.log.info("No customer email found.");
return reply
.code(200)
.send({ handled: false, requestId: request.id });
Expand Down
28 changes: 24 additions & 4 deletions src/api/sqs/handlers/provisionNewMember.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { AvailableSQSFunctions } from "common/types/sqsMessage.js";
import { currentEnvironmentConfig, SQSHandlerFunction } from "../index.js";
import { getEntraIdToken } from "../../../api/functions/entraId.js";
import { genericConfig } from "../../../common/config.js";
import { genericConfig, SecretConfig } from "../../../common/config.js";

import { setPaidMembership } from "api/functions/membership.js";
import {
MEMBER_CACHE_SECONDS,
setPaidMembership,
} from "api/functions/membership.js";
import { createAuditLogEntry } from "api/functions/auditLog.js";
import { Modules } from "common/modules.js";
import { getAuthorizedClients } from "../utils.js";
import { getAuthorizedClients, getSecretConfig } from "../utils.js";
import { emailMembershipPassHandler } from "./emailMembershipPassHandler.js";
import RedisModule from "ioredis";
import { setKey } from "api/functions/redisCache.js";

export const provisionNewMemberHandler: SQSHandlerFunction<
AvailableSQSFunctions.ProvisionNewMember
Expand All @@ -21,9 +26,16 @@ export const provisionNewMemberHandler: SQSHandlerFunction<
secretName: genericConfig.EntraSecretName,
logger,
});
const secretConfig: SecretConfig = await getSecretConfig({
logger,
commonConfig,
});
const redisClient = new RedisModule.default(secretConfig.redis_url);
const netId = email.replace("@illinois.edu", "");
const cacheKey = `membership:${netId}:acmpaid`;
logger.info("Got authorized clients and Entra ID token.");
const { updated } = await setPaidMembership({
netId: email.replace("@illinois.edu", ""),
netId,
dynamoClient: clients.dynamoClient,
entraToken,
paidMemberGroup: currentEnvironmentConfig.PaidMemberGroupId,
Expand All @@ -45,4 +57,12 @@ export const provisionNewMemberHandler: SQSHandlerFunction<
} else {
logger.info(`${email} was already a paid member.`);
}
logger.info("Setting membership in Redis.");
await setKey({
redisClient,
key: cacheKey,
data: JSON.stringify({ isMember: true }),
expiresIn: MEMBER_CACHE_SECONDS,
logger,
});
};
Loading