From dc91ac35ad1daed6886084dbb1a4f741ac178469 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 8 Mar 2025 14:46:31 -0600 Subject: [PATCH 1/6] port code from old branch --- cloudformation/iam.yml | 182 ++++++++++++++++---------------- cloudformation/main.yml | 16 +++ src/api/functions/validation.ts | 5 + src/api/index.ts | 2 + src/api/routes/membership.ts | 34 ++++++ src/common/config.ts | 3 + 6 files changed, 152 insertions(+), 90 deletions(-) create mode 100644 src/api/routes/membership.ts diff --git a/cloudformation/iam.yml b/cloudformation/iam.yml index 6493af73..fbeb9e3a 100644 --- a/cloudformation/iam.yml +++ b/cloudformation/iam.yml @@ -1,12 +1,12 @@ -AWSTemplateFormatVersion: '2010-09-09' +AWSTemplateFormatVersion: "2010-09-09" Description: Stack IAM Roles Transform: AWS::Serverless-2016-10-31 Parameters: RunEnvironment: Type: String AllowedValues: - - dev - - prod + - dev + - prod LambdaFunctionName: Type: String AllowedPattern: ^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$ @@ -21,99 +21,101 @@ Resources: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole AssumeRolePolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - - Action: - - sts:AssumeRole - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Policies: - - PolicyDocument: - Version: '2012-10-17' - Statement: - - Action: - - ses:SendEmail - - ses:SendRawEmail - Effect: Allow - Resource: "*" - Condition: - StringEquals: - ses:FromAddress: !Sub "membership@${SesEmailDomain}" - ForAllValues:StringLike: - ses:Recipients: - - "*@illinois.edu" - PolicyName: ses-membership - - PolicyDocument: - Version: '2012-10-17' - Statement: - Action: - - sqs:SendMessage + - sts:AssumeRole Effect: Allow - Resource: !Ref SqsQueueArn - PolicyName: lambda-sqs - - PolicyDocument: - Version: '2012-10-17' - Statement: - - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Effect: Allow - Resource: - - Fn::Sub: arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunctionName}:* - - Effect: Allow - Action: - - ec2:CreateNetworkInterface - - ec2:DescribeNetworkInterfaces - - ec2:DeleteNetworkInterface - - ec2:DescribeSubnets - - ec2:DeleteNetworkInterface - - ec2:AssignPrivateIpAddresses - - ec2:UnassignPrivateIpAddresses - Resource: '*' - PolicyName: lambda - - PolicyDocument: - Version: 2012-10-17 - Statement: - - Action: - - secretsmanager:GetSecretValue - Effect: Allow - Resource: - - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:infra-core-api-config* - PolicyName: lambda-db-secrets - - PolicyDocument: - Version: 2012-10-17 - Statement: - - Action: - - dynamodb:* - Effect: Allow - Resource: - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-events/* - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-events - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-cache - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-cache/* - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-purchase-history/* - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-purchase-history - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-tickets - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-tickets/* - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-ticketing-metadata/* - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-ticketing-metadata - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-metadata/* - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-metadata - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-userroles - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-userroles/* - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles/* - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links - - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links/* + Principal: + Service: + - lambda.amazonaws.com + Policies: + - PolicyDocument: + Version: "2012-10-17" + Statement: + - Action: + - ses:SendEmail + - ses:SendRawEmail + Effect: Allow + Resource: "*" + Condition: + StringEquals: + ses:FromAddress: !Sub "membership@${SesEmailDomain}" + ForAllValues:StringLike: + ses:Recipients: + - "*@illinois.edu" + PolicyName: ses-membership + - PolicyDocument: + Version: "2012-10-17" + Statement: + - Action: + - sqs:SendMessage + Effect: Allow + Resource: !Ref SqsQueueArn + PolicyName: lambda-sqs + - PolicyDocument: + Version: "2012-10-17" + Statement: + - Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Effect: Allow + Resource: + - Fn::Sub: arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunctionName}:* + - Effect: Allow + Action: + - ec2:CreateNetworkInterface + - ec2:DescribeNetworkInterfaces + - ec2:DeleteNetworkInterface + - ec2:DescribeSubnets + - ec2:DeleteNetworkInterface + - ec2:AssignPrivateIpAddresses + - ec2:UnassignPrivateIpAddresses + Resource: "*" + PolicyName: lambda + - PolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - secretsmanager:GetSecretValue + Effect: Allow + Resource: + - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:infra-core-api-config* + PolicyName: lambda-db-secrets + - PolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - dynamodb:* + Effect: Allow + Resource: + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-events/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-events + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-cache + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-cache/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-purchase-history/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-purchase-history + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-tickets + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-tickets/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-ticketing-metadata/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-events-ticketing-metadata + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-metadata/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-metadata + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-userroles + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-userroles/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links/* + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-membership-provisioning + - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-membership-provisioning/* - PolicyName: lambda-dynamo + PolicyName: lambda-dynamo Outputs: MainFunctionRoleArn: Description: Main API IAM role ARN Value: Fn::GetAtt: - - ApiLambdaIAMRole - - Arn + - ApiLambdaIAMRole + - Arn diff --git a/cloudformation/main.yml b/cloudformation/main.yml index ea770019..729693e7 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -253,6 +253,22 @@ Resources: FunctionResponseTypes: - ReportBatchItemFailures + MembershipRecordsTable: + Type: "AWS::DynamoDB::Table" + DeletionPolicy: "Retain" + Properties: + BillingMode: "PAY_PER_REQUEST" + TableName: infra-core-api-membership-provisioning + DeletionProtectionEnabled: true + PointInTimeRecoverySpecification: + PointInTimeRecoveryEnabled: !If [IsProd, true, false] + AttributeDefinitions: + - AttributeName: email + AttributeType: S + KeySchema: + - AttributeName: email + KeyType: HASH + IamGroupRolesTable: Type: "AWS::DynamoDB::Table" DeletionPolicy: "Retain" diff --git a/src/api/functions/validation.ts b/src/api/functions/validation.ts index b9f241f8..92729ef3 100644 --- a/src/api/functions/validation.ts +++ b/src/api/functions/validation.ts @@ -5,3 +5,8 @@ export function validateEmail(email: string): boolean { const result = emailSchema.safeParse(email); return result.success; } + +export function validateNetId(netId: string): boolean { + const regex = /^[a-zA-Z0-9\-]+$/; + return regex.test(netId); +} diff --git a/src/api/index.ts b/src/api/index.ts index d049bd6a..b2ff8eff 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -23,6 +23,7 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import mobileWalletRoute from "./routes/mobileWallet.js"; import stripeRoutes from "./routes/stripe.js"; +import membershipPlugin from "./routes/membership.js"; dotenv.config(); @@ -110,6 +111,7 @@ async function init() { api.register(protectedRoute, { prefix: "/protected" }); api.register(eventsPlugin, { prefix: "/events" }); api.register(organizationsPlugin, { prefix: "/organizations" }); + api.register(membershipPlugin, { prefix: "/membership" }); api.register(icalPlugin, { prefix: "/ical" }); api.register(iamRoutes, { prefix: "/iam" }); api.register(ticketsPlugin, { prefix: "/tickets" }); diff --git a/src/api/routes/membership.ts b/src/api/routes/membership.ts new file mode 100644 index 00000000..d7e452ae --- /dev/null +++ b/src/api/routes/membership.ts @@ -0,0 +1,34 @@ +import { validateNetId } from "api/functions/validation.js"; +import { NotImplementedError } from "common/errors/index.js"; +import { FastifyPluginAsync } from "fastify"; +import { ValidationError } from "zod-validation-error"; + +const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => { + fastify.get<{ + Body: undefined; + Querystring: { netId: string }; + }>( + "/:netId", + { + schema: { + querystring: { + type: "object", + properties: { + netId: { + type: "string", + }, + }, + }, + }, + }, + async (request, reply) => { + const netId = (request.params as Record).netId; + if (!validateNetId(netId)) { + throw new ValidationError(`${netId} is not a valid Illinois NetID!`); + } + throw new NotImplementedError({}); + }, + ); +}; + +export default membershipPlugin; diff --git a/src/common/config.ts b/src/common/config.ts index 51e27c67..36a3f9c2 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -17,6 +17,7 @@ export type ConfigType = { MembershipApiEndpoint: string; EmailDomain: string; SqsQueueUrl: string; + PaidMemberGroupId: string; }; export type GenericConfigType = { @@ -83,6 +84,7 @@ const environmentConfig: EnvironmentConfigType = { EmailDomain: "aws.qa.acmuiuc.org", SqsQueueUrl: "https://sqs.us-east-1.amazonaws.com/427040638965/infra-core-api-sqs", + PaidMemberGroupId: "9222451f-b354-4e64-ba28-c0f367a277c2", }, prod: { AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] }, @@ -100,6 +102,7 @@ const environmentConfig: EnvironmentConfigType = { EmailDomain: "acm.illinois.edu", SqsQueueUrl: "https://sqs.us-east-1.amazonaws.com/298118738376/infra-core-api-sqs", + PaidMemberGroupId: "172fd9ee-69f0-4384-9786-41ff1a43cf8e", }, }; From 64dce2cefc3c82e773e8095bcd69aa1a39a97b73 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 8 Mar 2025 14:48:06 -0600 Subject: [PATCH 2/6] update netid validation regex --- src/api/functions/validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/functions/validation.ts b/src/api/functions/validation.ts index 92729ef3..4c785c9d 100644 --- a/src/api/functions/validation.ts +++ b/src/api/functions/validation.ts @@ -7,6 +7,6 @@ export function validateEmail(email: string): boolean { } export function validateNetId(netId: string): boolean { - const regex = /^[a-zA-Z0-9\-]+$/; + const regex = /^[a-zA-Z]{2}[a-zA-Z\-]*(?:[2-9]|[1-9][0-9]{1,2})?$/; return regex.test(netId); } From 6361848387f01c0106075e9c5dff680543cb458b Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 8 Mar 2025 15:31:02 -0600 Subject: [PATCH 3/6] add membership checking to core --- src/api/functions/entraId.ts | 57 +++++++++++++++++++++++++++++++++ src/api/functions/membership.ts | 56 ++++++++++++++++++++++++++++++++ src/api/routes/membership.ts | 48 ++++++++++++++++++++++++--- src/api/routes/mobileWallet.ts | 3 +- src/common/config.ts | 2 ++ src/common/errors/index.ts | 1 - 6 files changed, 160 insertions(+), 7 deletions(-) diff --git a/src/api/functions/entraId.ts b/src/api/functions/entraId.ts index 00e3dd76..270f1b9c 100644 --- a/src/api/functions/entraId.ts +++ b/src/api/functions/entraId.ts @@ -447,3 +447,60 @@ export async function patchUserProfile( }); } } + +/** + * Checks if a user is a member of an Entra ID group. + * @param token - Entra ID token authorized to take this action. + * @param email - The email address of the user to check. + * @param group - The group ID to check membership in. + * @throws {EntraGroupError} If the membership check fails. + * @returns {Promise} True if the user is a member of the group, false otherwise. + */ +export async function isUserInGroup( + token: string, + email: string, + group: string, +): Promise { + email = email.toLowerCase().replace(/\s/g, ""); + if (!email.endsWith("@illinois.edu")) { + throw new EntraGroupError({ + group, + message: "User's domain must be illinois.edu to check group membership.", + }); + } + try { + const oid = await resolveEmailToOid(token, email); + const url = `https://graph.microsoft.com/v1.0/groups/${group}/members/${oid}`; + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (response.ok) { + return true; // User is in the group + } else if (response.status === 404) { + return false; // User is not in the group + } + + const errorData = (await response.json()) as { + error?: { message?: string }; + }; + throw new EntraGroupError({ + message: errorData?.error?.message ?? response.statusText, + group, + }); + } catch (error) { + if (error instanceof EntraGroupError) { + throw error; + } + const message = error instanceof Error ? error.message : String(error); + throw new EntraGroupError({ + message, + group, + }); + } +} diff --git a/src/api/functions/membership.ts b/src/api/functions/membership.ts index 7f446240..50c91ab3 100644 --- a/src/api/functions/membership.ts +++ b/src/api/functions/membership.ts @@ -1,4 +1,12 @@ +import { + DynamoDBClient, + PutItemCommand, + QueryCommand, +} from "@aws-sdk/client-dynamodb"; +import { marshall } from "@aws-sdk/util-dynamodb"; +import { genericConfig } from "common/config.js"; import { FastifyBaseLogger } from "fastify"; +import { isUserInGroup } from "./entraId.js"; export async function checkPaidMembership( endpoint: string, @@ -22,3 +30,51 @@ export async function checkPaidMembership( throw e; } } + +export async function checkPaidMembershipFromTable( + netId: string, + dynamoClient: DynamoDBClient, +): Promise { + const { Items } = await dynamoClient.send( + new QueryCommand({ + TableName: genericConfig.MembershipTableName, + KeyConditionExpression: "#pk = :pk", + ExpressionAttributeNames: { + "#pk": "email", + }, + ExpressionAttributeValues: marshall({ + ":pk": `${netId}@illinois.edu`, + }), + }), + ); + if (!Items || Items.length == 0) { + return false; + } + return true; +} + +export async function checkPaidMembershipFromEntra( + netId: string, + entraToken: string, + paidMemberGroup: string, +): Promise { + return isUserInGroup(entraToken, `${netId}@illinois.edu`, paidMemberGroup); +} + +export async function setPaidMembershipInTable( + netId: string, + dynamoClient: DynamoDBClient, +): Promise { + const obj = { + email: `${netId}@illinois.edu`, + inserted_at: new Date().toISOString(), + inserted_by: "membership-api-queried", + }; + + await dynamoClient.send( + new PutItemCommand({ + TableName: genericConfig.MembershipTableName, + Item: marshall(obj), + }), + ); +} diff --git a/src/api/routes/membership.ts b/src/api/routes/membership.ts index d7e452ae..63c25973 100644 --- a/src/api/routes/membership.ts +++ b/src/api/routes/membership.ts @@ -1,7 +1,12 @@ +import { + checkPaidMembershipFromEntra, + checkPaidMembershipFromTable, + setPaidMembershipInTable, +} from "api/functions/membership.js"; import { validateNetId } from "api/functions/validation.js"; -import { NotImplementedError } from "common/errors/index.js"; import { FastifyPluginAsync } from "fastify"; -import { ValidationError } from "zod-validation-error"; +import { ValidationError } from "common/errors/index.js"; +import { getEntraIdToken } from "api/functions/entraId.js"; const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => { fastify.get<{ @@ -24,9 +29,44 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => { async (request, reply) => { const netId = (request.params as Record).netId; if (!validateNetId(netId)) { - throw new ValidationError(`${netId} is not a valid Illinois NetID!`); + throw new ValidationError({ + message: `${netId} is not a valid Illinois NetID!`, + }); } - throw new NotImplementedError({}); + const isDynamoMember = await checkPaidMembershipFromTable( + netId, + fastify.dynamoClient, + ); + // check Dynamo cache first + if (isDynamoMember) { + return reply + .header("X-ACM-Data-Source", "dynamo") + .send({ netId, isPaidMember: true }); + } + // check AAD + const entraIdToken = await getEntraIdToken( + { + smClient: fastify.secretsManagerClient, + dynamoClient: fastify.dynamoClient, + }, + fastify.environmentConfig.AadValidClientId, + ); + const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId; + const isAadMember = await checkPaidMembershipFromEntra( + netId, + entraIdToken, + paidMemberGroup, + ); + if (isAadMember) { + reply + .header("X-ACM-Data-Source", "aad") + .send({ netId, isPaidMember: true }); + await setPaidMembershipInTable(netId, fastify.dynamoClient); + return; + } + return reply + .header("X-ACM-Data-Source", "aad") + .send({ netId, isPaidMember: false }); }, ); }; diff --git a/src/api/routes/mobileWallet.ts b/src/api/routes/mobileWallet.ts index 343563d5..c9b951f3 100644 --- a/src/api/routes/mobileWallet.ts +++ b/src/api/routes/mobileWallet.ts @@ -54,9 +54,8 @@ const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => { }); } const isPaidMember = await checkPaidMembership( - fastify.environmentConfig.MembershipApiEndpoint, - request.log, request.query.email.replace("@illinois.edu", ""), + fastify.dynamoClient, ); if (!isPaidMember) { throw new UnauthenticatedError({ diff --git a/src/common/config.ts b/src/common/config.ts index 36a3f9c2..6c31cbd4 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -31,6 +31,7 @@ export type GenericConfigType = { MerchStorePurchasesTableName: string; TicketPurchasesTableName: string; TicketMetadataTableName: string; + MembershipTableName: string; MerchStoreMetadataTableName: string; IAMTablePrefix: string; ProtectedEntraIDGroups: string[]; // these groups are too privileged to be modified via this portal and must be modified directly in Entra ID. @@ -63,6 +64,7 @@ const genericConfig: GenericConfigType = { TicketMetadataTableName: "infra-events-ticketing-metadata", IAMTablePrefix: "infra-core-api-iam", ProtectedEntraIDGroups: [infraChairsGroupId, officersGroupId], + MembershipTableName: "infra-core-api-membership-provisioning", } as const; const environmentConfig: EnvironmentConfigType = { diff --git a/src/common/errors/index.ts b/src/common/errors/index.ts index de495a22..2204f2b6 100644 --- a/src/common/errors/index.ts +++ b/src/common/errors/index.ts @@ -215,7 +215,6 @@ export class EntraFetchError extends BaseError<"EntraFetchError"> { } } - export class EntraPatchError extends BaseError<"EntraPatchError"> { email: string; constructor({ message, email }: { message?: string; email: string }) { From e546840c0b50791ffe00aad7fc5da3eb19b9e803 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 8 Mar 2025 15:37:33 -0600 Subject: [PATCH 4/6] revert mobilewallet --- src/api/routes/mobileWallet.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/routes/mobileWallet.ts b/src/api/routes/mobileWallet.ts index c9b951f3..343563d5 100644 --- a/src/api/routes/mobileWallet.ts +++ b/src/api/routes/mobileWallet.ts @@ -54,8 +54,9 @@ const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => { }); } const isPaidMember = await checkPaidMembership( + fastify.environmentConfig.MembershipApiEndpoint, + request.log, request.query.email.replace("@illinois.edu", ""), - fastify.dynamoClient, ); if (!isPaidMember) { throw new UnauthenticatedError({ From 7e5011c4c4ef52b1260e49fb6b2c64d742d3a97d Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 8 Mar 2025 16:09:48 -0600 Subject: [PATCH 5/6] fix --- cloudformation/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index 729693e7..c1ca621d 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -256,6 +256,7 @@ Resources: MembershipRecordsTable: Type: "AWS::DynamoDB::Table" DeletionPolicy: "Retain" + UpdateReplacePolicy: "Retain" Properties: BillingMode: "PAY_PER_REQUEST" TableName: infra-core-api-membership-provisioning From 4577594d940b075da090a3b3e812fc78cac56525 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 8 Mar 2025 16:24:33 -0600 Subject: [PATCH 6/6] fix test case handling --- src/api/routes/mobileWallet.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/api/routes/mobileWallet.ts b/src/api/routes/mobileWallet.ts index 343563d5..c1f570ae 100644 --- a/src/api/routes/mobileWallet.ts +++ b/src/api/routes/mobileWallet.ts @@ -53,11 +53,14 @@ const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => { message: "Email query parameter is not a valid email", }); } - const isPaidMember = await checkPaidMembership( - fastify.environmentConfig.MembershipApiEndpoint, - request.log, - request.query.email.replace("@illinois.edu", ""), - ); + const isPaidMember = + (fastify.runEnvironment === "dev" && + request.query.email === "testinguser@illinois.edu") || + (await checkPaidMembership( + fastify.environmentConfig.MembershipApiEndpoint, + request.log, + request.query.email.replace("@illinois.edu", ""), + )); if (!isPaidMember) { throw new UnauthenticatedError({ message: `${request.query.email} is not a paid member.`,