Skip to content

Commit cdd979e

Browse files
committed
build route
1 parent a2e8318 commit cdd979e

File tree

4 files changed

+134
-7
lines changed

4 files changed

+134
-7
lines changed

.eslintrc

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"plugin:prettier/recommended",
44
"plugin:@typescript-eslint/recommended"
55
],
6-
"plugins": ["import"],
6+
"plugins": [
7+
"import",
8+
"prettier"
9+
],
710
"rules": {
811
"import/no-unresolved": "error",
912
"import/extensions": [
@@ -32,27 +35,38 @@
3235
},
3336
"settings": {
3437
"import/parsers": {
35-
"@typescript-eslint/parser": [".ts", ".tsx", ".js", ".jsx"]
38+
"@typescript-eslint/parser": [
39+
".ts",
40+
".tsx",
41+
".js",
42+
".jsx"
43+
]
3644
},
3745
"import/resolver": {
3846
"typescript": {
3947
"alwaysTryTypes": true,
4048
"project": [
4149
"src/api/tsconfig.json", // Path to tsconfig.json in src/api
42-
"src/ui/tsconfig.json" // Path to tsconfig.json in src/ui
50+
"src/ui/tsconfig.json" // Path to tsconfig.json in src/ui
4351
]
4452
}
4553
}
4654
},
4755
"overrides": [
4856
{
49-
"files": ["*.test.ts", "*.testdata.ts"],
57+
"files": [
58+
"*.test.ts",
59+
"*.testdata.ts"
60+
],
5061
"rules": {
5162
"@typescript-eslint/no-explicit-any": "off"
5263
}
5364
},
5465
{
55-
"files": ["src/ui/*", "src/ui/**/*"],
66+
"files": [
67+
"src/ui/*",
68+
"src/ui/**/*"
69+
],
5670
"rules": {
5771
"@typescript-eslint/no-explicit-any": "off",
5872
"@typescript-eslint/no-unused-vars": "off"

src/api/functions/stripe.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type StripeCheckoutSessionCreateParams = {
1616
customerEmail?: string;
1717
stripeApiKey: string;
1818
items: { price: string; quantity: number }[];
19+
initiator: string;
1920
};
2021

2122
/**
@@ -69,6 +70,7 @@ export const createCheckoutSession = async ({
6970
stripeApiKey,
7071
customerEmail,
7172
items,
73+
initiator,
7274
}: StripeCheckoutSessionCreateParams): Promise<string> => {
7375
const stripe = new Stripe(stripeApiKey);
7476
const payload: Stripe.Checkout.SessionCreateParams = {
@@ -81,6 +83,9 @@ export const createCheckoutSession = async ({
8183
})),
8284
mode: "payment",
8385
customer_email: customerEmail,
86+
metadata: {
87+
initiator,
88+
},
8489
};
8590
const session = await stripe.checkout.sessions.create(payload);
8691
if (!session.url) {

src/api/routes/membership.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
} from "api/functions/membership.js";
66
import { validateNetId } from "api/functions/validation.js";
77
import { FastifyPluginAsync } from "fastify";
8-
import { InternalServerError, ValidationError } from "common/errors/index.js";
8+
import {
9+
BaseError,
10+
InternalServerError,
11+
UnauthenticatedError,
12+
ValidationError,
13+
} from "common/errors/index.js";
914
import { getEntraIdToken } from "api/functions/entraId.js";
1015
import { genericConfig, roleArns } from "common/config.js";
1116
import { getRoleCredentials } from "api/functions/sts.js";
@@ -14,6 +19,9 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
1419
import rateLimiter from "api/plugins/rateLimiter.js";
1520
import { createCheckoutSession } from "api/functions/stripe.js";
1621
import { getSecretValue } from "api/plugins/auth.js";
22+
import stripe, { Stripe } from "stripe";
23+
import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js";
24+
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
1725

1826
const NONMEMBER_CACHE_SECONDS = 1800; // 30 minutes
1927
const MEMBER_CACHE_SECONDS = 43200; // 12 hours
@@ -58,7 +66,7 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
5866
fastify.get<{
5967
Body: undefined;
6068
Querystring: { netId: string };
61-
}>("/checkoutSession/:netId", async (request, reply) => {
69+
}>("/checkout/:netId", async (request, reply) => {
6270
const netId = (request.params as Record<string, string>).netId;
6371
if (!validateNetId(netId)) {
6472
throw new ValidationError({
@@ -124,6 +132,7 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
124132
items: [
125133
{ price: fastify.environmentConfig.PaidMemberPriceId, quantity: 1 },
126134
],
135+
initiator: "purchase-membership",
127136
}),
128137
);
129138
});
@@ -181,6 +190,104 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
181190
.send({ netId, isPaidMember: false });
182191
});
183192
};
193+
194+
fastify.post(
195+
"/provision",
196+
{
197+
preParsing: async (request, _reply, payload) => {
198+
try {
199+
const sig = request.headers["stripe-signature"];
200+
if (!sig || typeof sig !== "string") {
201+
throw new Error("Missing or invalid Stripe signature");
202+
}
203+
204+
if (!Buffer.isBuffer(payload) && typeof payload !== "string") {
205+
throw new Error("Invalid payload format");
206+
}
207+
const secretApiConfig =
208+
(await getSecretValue(
209+
fastify.secretsManagerClient,
210+
genericConfig.ConfigSecretName,
211+
)) || {};
212+
if (!secretApiConfig) {
213+
throw new InternalServerError({
214+
message: "Could not connect to Stripe.",
215+
});
216+
}
217+
stripe.webhooks.constructEvent(
218+
payload.toString(),
219+
sig,
220+
secretApiConfig.stripe_endpoint_secret as string,
221+
);
222+
} catch (err: unknown) {
223+
if (err instanceof BaseError) {
224+
throw err;
225+
}
226+
throw new UnauthenticatedError({
227+
message: "Stripe webhook could not be validated.",
228+
});
229+
}
230+
},
231+
},
232+
async (request, reply) => {
233+
const event = request.body as Stripe.Event;
234+
switch (event.type) {
235+
case "checkout.session.completed":
236+
if (
237+
event.data.object.metadata &&
238+
"initiator" in event.data.object.metadata &&
239+
event.data.object.metadata["initiator"] == "purchase-membership"
240+
) {
241+
const customerEmail = event.data.object.customer_email;
242+
if (!customerEmail) {
243+
return reply
244+
.code(200)
245+
.send({ handled: false, requestId: request.id });
246+
}
247+
const sqsPayload: SQSPayload<AvailableSQSFunctions.ProvisionNewMember> =
248+
{
249+
function: AvailableSQSFunctions.ProvisionNewMember,
250+
metadata: {
251+
initiator: event.data.object.id,
252+
reqId: request.id,
253+
},
254+
payload: {
255+
email: customerEmail,
256+
},
257+
};
258+
if (!fastify.sqsClient) {
259+
fastify.sqsClient = new SQSClient({
260+
region: genericConfig.AwsRegion,
261+
});
262+
}
263+
const result = await fastify.sqsClient.send(
264+
new SendMessageCommand({
265+
QueueUrl: fastify.environmentConfig.SqsQueueUrl,
266+
MessageBody: JSON.stringify(sqsPayload),
267+
}),
268+
);
269+
if (!result.MessageId) {
270+
request.log.error(result);
271+
throw new InternalServerError({
272+
message: "Could not add job to queue.",
273+
});
274+
}
275+
return reply.status(200).send({
276+
handled: true,
277+
requestId: request.id,
278+
queueId: result.MessageId,
279+
});
280+
} else {
281+
return reply
282+
.code(200)
283+
.send({ handled: false, requestId: request.id });
284+
}
285+
default:
286+
request.log.warn(`Unhandled event type: ${event.type}`);
287+
}
288+
return reply.code(200).send({ handled: false, requestId: request.id });
289+
},
290+
);
184291
fastify.register(limitedRoutes);
185292
};
186293

src/common/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export type SecretConfig = {
123123
acm_passkit_signerKey_base64: string;
124124
apple_signing_cert_base64: string;
125125
stripe_secret_key: string;
126+
stripe_endpoint_secret: string;
126127
};
127128

128129
const roleArns = {

0 commit comments

Comments
 (0)