Skip to content

Commit b7062b3

Browse files
committed
move membership pass creation into SQS queue
1 parent c569f56 commit b7062b3

File tree

11 files changed

+148
-70
lines changed

11 files changed

+148
-70
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"scripts": {
1212
"build": "yarn workspaces run build && yarn lockfile-manage",
1313
"dev": "concurrently --names 'api,ui' 'yarn workspace infra-core-api run dev' 'yarn workspace infra-core-ui run dev'",
14-
"lockfile-manage": "synp --with-workspace --source-file yarn.lock && cp package-lock.json dist/lambda/ && cp src/api/package.lambda.json dist/lambda/package.json && rm package-lock.json",
14+
"lockfile-manage": "synp --with-workspace --source-file yarn.lock && cp package-lock.json dist/lambda/ && cp package-lock.json dist/sqsConsumer/ && cp src/api/package.lambda.json dist/lambda/package.json && cp src/api/package.lambda.json dist/sqsConsumer/package.json && rm package-lock.json",
1515
"prettier": "yarn workspaces run prettier && prettier --check tests/**/*.ts",
1616
"prettier:write": "yarn workspaces run prettier:write && prettier --write tests/**/*.ts",
1717
"lint": "yarn workspaces run lint",
@@ -81,4 +81,4 @@
8181
"resolutions": {
8282
"pdfjs-dist": "^4.8.69"
8383
}
84-
}
84+
}

src/api/functions/entraId.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,22 @@ import {
2121
} from "../../common/types/iam.js";
2222
import { FastifyInstance } from "fastify";
2323
import { UserProfileDataBase } from "common/types/msGraphApi.js";
24+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
25+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2426

2527
function validateGroupId(groupId: string): boolean {
2628
const groupIdPattern = /^[a-zA-Z0-9-]+$/; // Adjust the pattern as needed
2729
return groupIdPattern.test(groupId);
2830
}
2931

3032
export async function getEntraIdToken(
31-
fastify: FastifyInstance,
33+
clients: { smClient: SecretsManagerClient; dynamoClient: DynamoDBClient },
3234
clientId: string,
3335
scopes: string[] = ["https://graph.microsoft.com/.default"],
3436
) {
3537
const secretApiConfig =
36-
(await getSecretValue(
37-
fastify.secretsManagerClient,
38-
genericConfig.ConfigSecretName,
39-
)) || {};
38+
(await getSecretValue(clients.smClient, genericConfig.ConfigSecretName)) ||
39+
{};
4040
if (
4141
!secretApiConfig.entra_id_private_key ||
4242
!secretApiConfig.entra_id_thumbprint
@@ -50,7 +50,7 @@ export async function getEntraIdToken(
5050
"base64",
5151
).toString("utf8");
5252
const cachedToken = await getItemFromCache(
53-
fastify.dynamoClient,
53+
clients.dynamoClient,
5454
"entra_id_access_token",
5555
);
5656
if (cachedToken) {
@@ -80,7 +80,7 @@ export async function getEntraIdToken(
8080
date.setTime(date.getTime() - 30000);
8181
if (result?.accessToken) {
8282
await insertItemIntoCache(
83-
fastify.dynamoClient,
83+
clients.dynamoClient,
8484
"entra_id_access_token",
8585
{ token: result?.accessToken },
8686
date,

src/api/functions/mobileWallet.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { getSecretValue } from "../plugins/auth.js";
2-
import { genericConfig, SecretConfig } from "../../common/config.js";
2+
import {
3+
ConfigType,
4+
genericConfig,
5+
GenericConfigType,
6+
SecretConfig,
7+
} from "../../common/config.js";
38
import {
49
InternalServerError,
510
UnauthorizedError,
@@ -12,6 +17,9 @@ import strip from "../resources/MembershipPass.pkpass/strip.png";
1217
import pass from "../resources/MembershipPass.pkpass/pass.js";
1318
import { PKPass } from "passkit-generator";
1419
import { promises as fs } from "fs";
20+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
21+
import { RunEnvironment } from "common/roles.js";
22+
import pino from "pino";
1523

1624
function trim(s: string) {
1725
return (s || "").replace(/^\s+|\s+$/g, "");
@@ -25,9 +33,11 @@ function convertName(name: string): string {
2533
}
2634

2735
export async function issueAppleWalletMembershipCard(
28-
app: FastifyInstance,
29-
request: FastifyRequest,
36+
clients: { smClient: SecretsManagerClient },
37+
environmentConfig: ConfigType,
38+
runEnvironment: RunEnvironment,
3039
email: string,
40+
logger: pino.Logger,
3141
name?: string,
3242
) {
3343
if (!email.endsWith("@illinois.edu")) {
@@ -37,7 +47,7 @@ export async function issueAppleWalletMembershipCard(
3747
});
3848
}
3949
const secretApiConfig = (await getSecretValue(
40-
app.secretsManagerClient,
50+
clients.smClient,
4151
genericConfig.ConfigSecretName,
4252
)) as SecretConfig;
4353
if (!secretApiConfig) {
@@ -57,7 +67,7 @@ export async function issueAppleWalletMembershipCard(
5767
secretApiConfig.apple_signing_cert_base64,
5868
"base64",
5969
).toString("utf-8");
60-
pass["passTypeIdentifier"] = app.environmentConfig["PasskitIdentifier"];
70+
pass["passTypeIdentifier"] = environmentConfig["PasskitIdentifier"];
6171

6272
const pkpass = new PKPass(
6373
{
@@ -73,13 +83,13 @@ export async function issueAppleWalletMembershipCard(
7383
},
7484
{
7585
// logoText: app.runEnvironment === "dev" ? "INVALID Membership Pass" : "Membership Pass",
76-
serialNumber: app.environmentConfig["PasskitSerialNumber"],
86+
serialNumber: environmentConfig["PasskitSerialNumber"],
7787
},
7888
);
7989
pkpass.setBarcodes({
8090
altText: email.split("@")[0],
8191
format: "PKBarcodeFormatPDF417",
82-
message: app.runEnvironment === "dev" ? `INVALID${email}INVALID` : email,
92+
message: runEnvironment === "dev" ? `INVALID${email}INVALID` : email,
8393
});
8494
const iat = new Date().toLocaleDateString("en-US", {
8595
day: "2-digit",
@@ -93,7 +103,7 @@ export async function issueAppleWalletMembershipCard(
93103
value: convertName(name),
94104
});
95105
}
96-
if (app.runEnvironment === "prod") {
106+
if (runEnvironment === "prod") {
97107
pkpass.backFields.push({
98108
label: "Verification URL",
99109
key: "iss",
@@ -109,7 +119,7 @@ export async function issueAppleWalletMembershipCard(
109119
pkpass.backFields.push({ label: "Pass Created On", key: "iat", value: iat });
110120
pkpass.backFields.push({ label: "Membership ID", key: "id", value: email });
111121
const buffer = pkpass.getAsBuffer();
112-
request.log.info(
122+
logger.info(
113123
{ type: "audit", actor: email, target: email },
114124
"Created membership verification pass",
115125
);

src/api/routes/iam.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
160160
async (request, reply) => {
161161
const emails = request.body.emails;
162162
const entraIdToken = await getEntraIdToken(
163-
fastify,
163+
{
164+
smClient: fastify.secretsManagerClient,
165+
dynamoClient: fastify.dynamoClient,
166+
},
164167
fastify.environmentConfig.AadValidClientId,
165168
);
166169
if (!entraIdToken) {
@@ -247,7 +250,10 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
247250
});
248251
}
249252
const entraIdToken = await getEntraIdToken(
250-
fastify,
253+
{
254+
smClient: fastify.secretsManagerClient,
255+
dynamoClient: fastify.dynamoClient,
256+
},
251257
fastify.environmentConfig.AadValidClientId,
252258
);
253259
const addResults = await Promise.allSettled(
@@ -371,7 +377,10 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
371377
});
372378
}
373379
const entraIdToken = await getEntraIdToken(
374-
fastify,
380+
{
381+
smClient: fastify.secretsManagerClient,
382+
dynamoClient: fastify.dynamoClient,
383+
},
375384
fastify.environmentConfig.AadValidClientId,
376385
);
377386
const response = await listGroupMembers(entraIdToken, groupId);

src/api/routes/mobileWallet.ts

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
import { FastifyPluginAsync } from "fastify";
2-
import { issueAppleWalletMembershipCard } from "../functions/mobileWallet.js";
32
import {
4-
EntraFetchError,
3+
InternalServerError,
54
UnauthenticatedError,
6-
UnauthorizedError,
75
ValidationError,
86
} from "../../common/errors/index.js";
9-
import { generateMembershipEmailCommand } from "../functions/ses.js";
107
import { z } from "zod";
11-
import { getEntraIdToken, getUserProfile } from "../functions/entraId.js";
128
import { checkPaidMembership } from "../functions/membership.js";
9+
import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js";
10+
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
11+
import { genericConfig } from "common/config.js";
12+
import { zodToJsonSchema } from "zod-to-json-schema";
13+
14+
const queuedResponseJsonSchema = zodToJsonSchema(
15+
z.object({
16+
queueId: z.string().uuid(),
17+
}),
18+
);
1319

1420
const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => {
1521
fastify.post<{ Querystring: { email: string } }>(
1622
"/membership",
1723
{
1824
schema: {
25+
response: { 202: queuedResponseJsonSchema },
1926
querystring: {
2027
type: "object",
2128
properties: {
@@ -53,37 +60,36 @@ const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => {
5360
message: `${request.query.email} is not a paid member.`,
5461
});
5562
}
56-
const entraIdToken = await getEntraIdToken(
57-
fastify,
58-
fastify.environmentConfig.AadValidClientId,
59-
);
60-
61-
const userProfile = await getUserProfile(
62-
entraIdToken,
63-
request.query.email,
64-
);
65-
66-
const item = await issueAppleWalletMembershipCard(
67-
fastify,
68-
request,
69-
request.query.email,
70-
userProfile.displayName,
71-
);
72-
const emailCommand = generateMembershipEmailCommand(
73-
request.query.email,
74-
`membership@${fastify.environmentConfig.EmailDomain}`,
75-
item,
63+
const sqsPayload: SQSPayload<AvailableSQSFunctions.EmailMembershipPass> =
64+
{
65+
function: AvailableSQSFunctions.EmailMembershipPass,
66+
metadata: {
67+
initiator: "public",
68+
reqId: request.id,
69+
},
70+
payload: {
71+
email: request.query.email,
72+
},
73+
};
74+
if (!fastify.sqsClient) {
75+
fastify.sqsClient = new SQSClient({
76+
region: genericConfig.AwsRegion,
77+
});
78+
}
79+
const result = await fastify.sqsClient.send(
80+
new SendMessageCommand({
81+
QueueUrl: fastify.environmentConfig.SqsQueueUrl,
82+
MessageBody: JSON.stringify(sqsPayload),
83+
}),
7684
);
77-
if (
78-
fastify.runEnvironment === "dev" &&
79-
request.query.email === "[email protected]"
80-
) {
81-
return reply
82-
.status(202)
83-
.send({ message: "OK (skipped sending email)" });
85+
if (!result.MessageId) {
86+
request.log.error(result);
87+
throw new InternalServerError({
88+
message: "Could not add job to queue.",
89+
});
8490
}
85-
await fastify.sesClient.send(emailCommand);
86-
reply.status(202).send({ message: "OK" });
91+
request.log.info(`Queued job to SQS with message ID ${result.MessageId}`);
92+
reply.status(202).send({ queueId: result.MessageId });
8793
},
8894
);
8995
};

src/api/sqs/driver.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const payload = parseSQSPayload({
1111
function: "ping",
1212
payload: {},
1313
metadata: {
14-
taskId: "0",
1514
reqId: "1",
1615
initiator: "[email protected]",
1716
},

src/api/sqs/handlers.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,49 @@
11
import { AvailableSQSFunctions } from "common/types/sqsMessage.js";
2-
import { SQSHandlerFunction } from "./index.js";
2+
import {
3+
currentEnvironmentConfig,
4+
runEnvironment,
5+
SQSHandlerFunction,
6+
} from "./index.js";
7+
import { getEntraIdToken, getUserProfile } from "api/functions/entraId.js";
8+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
9+
import { environmentConfig, genericConfig } from "common/config.js";
10+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
11+
import { issueAppleWalletMembershipCard } from "api/functions/mobileWallet.js";
12+
import { generateMembershipEmailCommand } from "api/functions/ses.js";
13+
import { SESClient } from "@aws-sdk/client-ses";
314

415
export const emailMembershipPassHandler: SQSHandlerFunction<
516
AvailableSQSFunctions.EmailMembershipPass
6-
> = async (payload, metadata, logger) => {
7-
logger.error("Not implemented yet!");
17+
> = async (payload, _metadata, logger) => {
18+
const email = payload.email;
19+
const commonConfig = { region: genericConfig.AwsRegion };
20+
const clients = {
21+
smClient: new SecretsManagerClient(commonConfig),
22+
dynamoClient: new DynamoDBClient(commonConfig),
23+
sesClient: new SESClient(commonConfig),
24+
};
25+
const entraIdToken = await getEntraIdToken(
26+
clients,
27+
currentEnvironmentConfig.AadValidClientId,
28+
);
29+
const userProfile = await getUserProfile(entraIdToken, email);
30+
const pkpass = await issueAppleWalletMembershipCard(
31+
clients,
32+
environmentConfig[runEnvironment],
33+
runEnvironment,
34+
email,
35+
logger,
36+
userProfile.displayName,
37+
);
38+
const emailCommand = generateMembershipEmailCommand(
39+
email,
40+
`membership@${environmentConfig[runEnvironment].EmailDomain}`,
41+
pkpass,
42+
);
43+
if (runEnvironment === "dev" && email === "[email protected]") {
44+
return;
45+
}
46+
await clients.sesClient.send(emailCommand);
847
return;
948
};
1049

0 commit comments

Comments
 (0)