@@ -5,7 +5,12 @@ import {
55} from "api/functions/membership.js" ;
66import { validateNetId } from "api/functions/validation.js" ;
77import { 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" ;
914import { getEntraIdToken } from "api/functions/entraId.js" ;
1015import { genericConfig , roleArns } from "common/config.js" ;
1116import { getRoleCredentials } from "api/functions/sts.js" ;
@@ -14,6 +19,9 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
1419import rateLimiter from "api/plugins/rateLimiter.js" ;
1520import { createCheckoutSession } from "api/functions/stripe.js" ;
1621import { 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
1826const NONMEMBER_CACHE_SECONDS = 1800 ; // 30 minutes
1927const 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
0 commit comments