@@ -23,7 +23,8 @@ import { EntraGroupActions } from "common/types/iam.js";
2323import { buildAuditLogTransactPut } from "./auditLog.js" ;
2424import { Modules } from "common/modules.js" ;
2525import { 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" ;
2728
2829export interface GetOrgInfoInputs {
2930 id : string ;
@@ -184,6 +185,7 @@ export const addLead = async ({
184185 dynamoClient,
185186 logger,
186187 officersEmail,
188+ redisClient,
187189} : {
188190 user : z . infer < typeof enforcedOrgLeadEntry > ;
189191 orgId : string ;
@@ -194,6 +196,7 @@ export const addLead = async ({
194196 dynamoClient : DynamoDBClient ;
195197 logger : FastifyBaseLogger ;
196198 officersEmail : string ;
199+ redisClient : Redis ;
197200} ) : Promise < SQSMessage | null > => {
198201 const { username } = user ;
199202
@@ -229,51 +232,60 @@ export const addLead = async ({
229232
230233 return await dynamoClient . send ( addTransaction ) ;
231234 } ;
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+ }
232260
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.` ,
238263 ) ;
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+ ) ;
244273 logger . info (
245- `User ${ username } is already a lead for ${ orgId } . Skipping add operation .` ,
274+ `Successfully added ${ username } to Entra group for ${ orgId } .` ,
246275 ) ;
247- return null ;
248276 }
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- }
266277
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+ } ) ;
277289} ;
278290
279291export const removeLead = async ( {
@@ -286,6 +298,7 @@ export const removeLead = async ({
286298 dynamoClient,
287299 logger,
288300 officersEmail,
301+ redisClient,
289302} : {
290303 username : string ;
291304 orgId : string ;
@@ -296,6 +309,7 @@ export const removeLead = async ({
296309 dynamoClient : DynamoDBClient ;
297310 logger : FastifyBaseLogger ;
298311 officersEmail : string ;
312+ redisClient : Redis ;
299313} ) : Promise < SQSMessage | null > => {
300314 const removeOperation = async ( ) => {
301315 const removeTransaction = new TransactWriteItemsCommand ( {
@@ -325,52 +339,61 @@ export const removeLead = async ({
325339 return await dynamoClient . send ( removeTransaction ) ;
326340 } ;
327341
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 } ` ,
341355 ) ;
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 ;
343367 }
344- throw e ;
345- }
346-
347- logger . info (
348- `Successfully removed ${ username } as lead for ${ orgId } in DynamoDB.` ,
349- ) ;
350368
351- if ( entraGroupId ) {
352- await modifyGroup (
353- entraIdToken ,
354- username ,
355- entraGroupId ,
356- EntraGroupActions . REMOVE ,
357- dynamoClient ,
358- ) ;
359369 logger . info (
360- `Successfully removed ${ username } from Entra group for ${ orgId } .` ,
370+ `Successfully removed ${ username } as lead for ${ orgId } in DynamoDB .` ,
361371 ) ;
362- }
363372
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+ } ) ;
374397} ;
375398
376399/**
0 commit comments