@@ -23,7 +23,8 @@ import { EntraGroupActions } from "common/types/iam.js";
23
23
import { buildAuditLogTransactPut } from "./auditLog.js" ;
24
24
import { Modules } from "common/modules.js" ;
25
25
import { retryDynamoTransactionWithBackoff } from "api/utils.js" ;
26
- import { ValidLoggers } from "api/types.js" ;
26
+ import { Redis , ValidLoggers } from "api/types.js" ;
27
+ import { createLock , IoredisAdapter , type SimpleLock } from "redlock-universal" ;
27
28
28
29
export interface GetOrgInfoInputs {
29
30
id : string ;
@@ -184,6 +185,7 @@ export const addLead = async ({
184
185
dynamoClient,
185
186
logger,
186
187
officersEmail,
188
+ redisClient,
187
189
} : {
188
190
user : z . infer < typeof enforcedOrgLeadEntry > ;
189
191
orgId : string ;
@@ -194,6 +196,7 @@ export const addLead = async ({
194
196
dynamoClient : DynamoDBClient ;
195
197
logger : FastifyBaseLogger ;
196
198
officersEmail : string ;
199
+ redisClient : Redis ;
197
200
} ) : Promise < SQSMessage | null > => {
198
201
const { username } = user ;
199
202
@@ -229,51 +232,60 @@ export const addLead = async ({
229
232
230
233
return await dynamoClient . send ( addTransaction ) ;
231
234
} ;
235
+ const lock = createLock ( {
236
+ adapter : new IoredisAdapter ( redisClient ) ,
237
+ key : `user:${ username } ` ,
238
+ retryAttempts : 5 ,
239
+ retryDelay : 300 ,
240
+ } ) as SimpleLock ;
241
+ return await lock . using ( async ( ) => {
242
+ try {
243
+ await retryDynamoTransactionWithBackoff (
244
+ addOperation ,
245
+ logger ,
246
+ `Add lead ${ username } to ${ orgId } ` ,
247
+ ) ;
248
+ } catch ( e : any ) {
249
+ if (
250
+ e . name === "TransactionCanceledException" &&
251
+ e . message . includes ( "ConditionalCheckFailed" )
252
+ ) {
253
+ logger . info (
254
+ `User ${ username } is already a lead for ${ orgId } . Skipping add operation.` ,
255
+ ) ;
256
+ return null ;
257
+ }
258
+ throw e ;
259
+ }
232
260
233
- try {
234
- await retryDynamoTransactionWithBackoff (
235
- addOperation ,
236
- logger ,
237
- `Add lead ${ username } to ${ orgId } ` ,
261
+ logger . info (
262
+ `Successfully added ${ username } as lead for ${ orgId } in DynamoDB.` ,
238
263
) ;
239
- } catch ( e : any ) {
240
- if (
241
- e . name === "TransactionCanceledException" &&
242
- e . message . includes ( "ConditionalCheckFailed" )
243
- ) {
264
+
265
+ if ( entraGroupId ) {
266
+ await modifyGroup (
267
+ entraIdToken ,
268
+ username ,
269
+ entraGroupId ,
270
+ EntraGroupActions . ADD ,
271
+ dynamoClient ,
272
+ ) ;
244
273
logger . info (
245
- `User ${ username } is already a lead for ${ orgId } . Skipping add operation .` ,
274
+ `Successfully added ${ username } to Entra group for ${ orgId } .` ,
246
275
) ;
247
- return null ;
248
276
}
249
- throw e ;
250
- }
251
-
252
- logger . info (
253
- `Successfully added ${ username } as lead for ${ orgId } in DynamoDB.` ,
254
- ) ;
255
-
256
- if ( entraGroupId ) {
257
- await modifyGroup (
258
- entraIdToken ,
259
- username ,
260
- entraGroupId ,
261
- EntraGroupActions . ADD ,
262
- dynamoClient ,
263
- ) ;
264
- logger . info ( `Successfully added ${ username } to Entra group for ${ orgId } .` ) ;
265
- }
266
277
267
- return {
268
- function : AvailableSQSFunctions . EmailNotifications ,
269
- metadata : { initiator : actorUsername , reqId } ,
270
- payload : {
271
- to : getAllUserEmails ( username ) ,
272
- cc : [ officersEmail ] ,
273
- subject : `Lead added for ${ orgId } ` ,
274
- content : `Hello,\n\nWe're letting you know that ${ username } has been added as a lead for ${ orgId } by ${ actorUsername } . Changes may take up to 2 hours to reflect in all systems.\n\nNo action is required from you at this time.` ,
275
- } ,
276
- } ;
278
+ return {
279
+ function : AvailableSQSFunctions . EmailNotifications ,
280
+ metadata : { initiator : actorUsername , reqId } ,
281
+ payload : {
282
+ to : getAllUserEmails ( username ) ,
283
+ cc : [ officersEmail ] ,
284
+ subject : `${ user . nonVotingMember ? "Non-voting lead" : "Lead" } added for ${ orgId } ` ,
285
+ content : `Hello,\n\nWe're letting you know that ${ username } has been added as a ${ user . nonVotingMember ? "non-voting" : "" } lead for ${ orgId } by ${ actorUsername } . Changes may take up to 2 hours to reflect in all systems.` ,
286
+ } ,
287
+ } ;
288
+ } ) ;
277
289
} ;
278
290
279
291
export const removeLead = async ( {
@@ -286,6 +298,7 @@ export const removeLead = async ({
286
298
dynamoClient,
287
299
logger,
288
300
officersEmail,
301
+ redisClient,
289
302
} : {
290
303
username : string ;
291
304
orgId : string ;
@@ -296,6 +309,7 @@ export const removeLead = async ({
296
309
dynamoClient : DynamoDBClient ;
297
310
logger : FastifyBaseLogger ;
298
311
officersEmail : string ;
312
+ redisClient : Redis ;
299
313
} ) : Promise < SQSMessage | null > => {
300
314
const removeOperation = async ( ) => {
301
315
const removeTransaction = new TransactWriteItemsCommand ( {
@@ -325,52 +339,61 @@ export const removeLead = async ({
325
339
return await dynamoClient . send ( removeTransaction ) ;
326
340
} ;
327
341
328
- try {
329
- await retryDynamoTransactionWithBackoff (
330
- removeOperation ,
331
- logger ,
332
- `Remove lead ${ username } from ${ orgId } ` ,
333
- ) ;
334
- } catch ( e : any ) {
335
- if (
336
- e . name === "TransactionCanceledException" &&
337
- e . message . includes ( "ConditionalCheckFailed" )
338
- ) {
339
- logger . info (
340
- `User ${ username } was not a lead for ${ orgId } . Skipping remove operation. ` ,
342
+ const lock = createLock ( {
343
+ adapter : new IoredisAdapter ( redisClient ) ,
344
+ key : `user: ${ username } ` ,
345
+ retryAttempts : 5 ,
346
+ retryDelay : 300 ,
347
+ } ) as SimpleLock ;
348
+
349
+ return await lock . using ( async ( ) => {
350
+ try {
351
+ await retryDynamoTransactionWithBackoff (
352
+ removeOperation ,
353
+ logger ,
354
+ `Remove lead ${ username } from ${ orgId } ` ,
341
355
) ;
342
- return null ;
356
+ } catch ( e : any ) {
357
+ if (
358
+ e . name === "TransactionCanceledException" &&
359
+ e . message . includes ( "ConditionalCheckFailed" )
360
+ ) {
361
+ logger . info (
362
+ `User ${ username } was not a lead for ${ orgId } . Skipping remove operation.` ,
363
+ ) ;
364
+ return null ;
365
+ }
366
+ throw e ;
343
367
}
344
- throw e ;
345
- }
346
-
347
- logger . info (
348
- `Successfully removed ${ username } as lead for ${ orgId } in DynamoDB.` ,
349
- ) ;
350
368
351
- if ( entraGroupId ) {
352
- await modifyGroup (
353
- entraIdToken ,
354
- username ,
355
- entraGroupId ,
356
- EntraGroupActions . REMOVE ,
357
- dynamoClient ,
358
- ) ;
359
369
logger . info (
360
- `Successfully removed ${ username } from Entra group for ${ orgId } .` ,
370
+ `Successfully removed ${ username } as lead for ${ orgId } in DynamoDB .` ,
361
371
) ;
362
- }
363
372
364
- return {
365
- function : AvailableSQSFunctions . EmailNotifications ,
366
- metadata : { initiator : actorUsername , reqId } ,
367
- payload : {
368
- to : getAllUserEmails ( username ) ,
369
- cc : [ officersEmail ] ,
370
- subject : `Lead removed for ${ orgId } ` ,
371
- content : `Hello,\n\nWe're letting you know that ${ username } has been removed as a lead for ${ orgId } by ${ actorUsername } .\n\nNo action is required from you at this time.` ,
372
- } ,
373
- } ;
373
+ if ( entraGroupId ) {
374
+ await modifyGroup (
375
+ entraIdToken ,
376
+ username ,
377
+ entraGroupId ,
378
+ EntraGroupActions . REMOVE ,
379
+ dynamoClient ,
380
+ ) ;
381
+ logger . info (
382
+ `Successfully removed ${ username } from Entra group for ${ orgId } .` ,
383
+ ) ;
384
+ }
385
+
386
+ return {
387
+ function : AvailableSQSFunctions . EmailNotifications ,
388
+ metadata : { initiator : actorUsername , reqId } ,
389
+ payload : {
390
+ to : getAllUserEmails ( username ) ,
391
+ cc : [ officersEmail ] ,
392
+ subject : `Lead removed for ${ orgId } ` ,
393
+ content : `Hello,\n\nWe're letting you know that ${ username } has been removed as a lead for ${ orgId } by ${ actorUsername } .\n\nNo action is required from you at this time.` ,
394
+ } ,
395
+ } ;
396
+ } ) ;
374
397
} ;
375
398
376
399
/**
0 commit comments