From e09b763e50aca6f298618fd887e379a25dfa2cf4 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Thu, 27 Nov 2025 20:42:56 +0530 Subject: [PATCH] fix: review comments --- backend/src/ee/services/scim/scim-dal.ts | 55 ++++++++----------- backend/src/ee/services/scim/scim-service.ts | 7 ++- .../emails/ScimTokenExpiryNoticeTemplate.tsx | 23 +++++++- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/backend/src/ee/services/scim/scim-dal.ts b/backend/src/ee/services/scim/scim-dal.ts index 580ae2747..e856070e1 100644 --- a/backend/src/ee/services/scim/scim-dal.ts +++ b/backend/src/ee/services/scim/scim-dal.ts @@ -14,44 +14,35 @@ export const scimDALFactory = (db: TDbClient) => { const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0): Promise => { try { - const conn = tx || db.replicaNode(); - - const batch = await conn(TableName.ScimToken) - .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.ScimToken}.orgId`) + const batch = await (tx || db.replicaNode())(TableName.ScimToken) + .leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.ScimToken}.orgId`) + .leftJoin(TableName.Membership, `${TableName.Membership}.scopeOrgId`, `${TableName.ScimToken}.orgId`) + .leftJoin(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) .whereRaw( ` (${TableName.ScimToken}."ttlDays" > 0 AND - (${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") < NOW() + INTERVAL '1 day' AND - (${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") > NOW()) + (${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") < NOW() + INTERVAL '7 days' AND + (${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") > NOW()) ` ) .where(`${TableName.ScimToken}.expiryNotificationSent`, false) - .select( - conn.ref("id").withSchema(TableName.ScimToken), - conn.ref("ttlDays").withSchema(TableName.ScimToken), - conn.ref("description").withSchema(TableName.ScimToken), - conn.ref("orgId").withSchema(TableName.ScimToken), - conn.ref("createdAt").withSchema(TableName.ScimToken), - conn.ref("name").withSchema(TableName.Organization).as("orgName"), - conn.raw(` - COALESCE( - ( - SELECT array_agg(${TableName.Users}.email) - FROM ${TableName.Membership} - JOIN ${TableName.MembershipRole} ON ${TableName.Membership}.id = ${TableName.MembershipRole}."membershipId" - JOIN ${TableName.Users} ON ${TableName.Membership}."actorUserId" = ${TableName.Users}.id - WHERE ${TableName.Membership}."scopeOrgId" = ${TableName.ScimToken}."orgId" - AND ${TableName.Membership}.scope = '${AccessScope.Organization}' - AND ${TableName.MembershipRole}.role = '${OrgMembershipRole.Admin}' - AND ${TableName.Membership}.status != '${OrgMembershipStatus.Invited}' - AND ${TableName.Membership}."actorUserId" IS NOT NULL - AND ${TableName.Users}."isGhost" = false - AND ${TableName.Users}.email IS NOT NULL - ), - ARRAY[]::text[] - ) as "adminEmails" - `) - ) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.MembershipRole}.role`, OrgMembershipRole.Admin) + .whereNot(`${TableName.Membership}.status`, OrgMembershipStatus.Invited) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Users}.isGhost`, false) + .whereNotNull(`${TableName.Users}.email`) + .groupBy([`${TableName.ScimToken}.id`, `${TableName.Organization}.name`]) + .select([ + db.ref("id").withSchema(TableName.ScimToken), + db.ref("ttlDays").withSchema(TableName.ScimToken), + db.ref("description").withSchema(TableName.ScimToken), + db.ref("orgId").withSchema(TableName.ScimToken), + db.ref("createdAt").withSchema(TableName.ScimToken), + db.ref("name").withSchema(TableName.Organization).as("orgName"), + db.raw(`array_agg(${TableName.Users}."email") as "adminEmails"`) + ]) .limit(batchSize) .offset(offset); diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 0f71dd185..0542bc911 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -1266,6 +1266,9 @@ export const scimServiceFactory = ({ return; } + const createdOn = new Date(token.createdAt); + const expiringOn = new Date(createdOn.getTime() + Number(token.ttlDays) * 86400 * 1000); + await smtpService.sendMail({ recipients: token.adminEmails, subjectLine: "SCIM Token Expiry Notice", @@ -1273,7 +1276,9 @@ export const scimServiceFactory = ({ substitutions: { tokenDescription: token.description, orgName: token.orgName, - url: `${appCfg.SITE_URL}/organizations/${token.orgId}/settings?selectedTab=provisioning-settings` + url: `${appCfg.SITE_URL}/organizations/${token.orgId}/settings?selectedTab=provisioning-settings`, + createdOn, + expiringOn } }); diff --git a/backend/src/services/smtp/emails/ScimTokenExpiryNoticeTemplate.tsx b/backend/src/services/smtp/emails/ScimTokenExpiryNoticeTemplate.tsx index afc4bc90c..84b9395b2 100644 --- a/backend/src/services/smtp/emails/ScimTokenExpiryNoticeTemplate.tsx +++ b/backend/src/services/smtp/emails/ScimTokenExpiryNoticeTemplate.tsx @@ -7,6 +7,8 @@ import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface ScimTokenExpiryNoticeTemplateProps extends Omit { tokenDescription?: string; orgName: string; + createdOn: Date; + expiringOn: Date; url: string; } @@ -14,8 +16,20 @@ export const ScimTokenExpiryNoticeTemplate = ({ tokenDescription, siteUrl, orgName, - url + url, + createdOn, + expiringOn }: ScimTokenExpiryNoticeTemplateProps) => { + const formatDate = (date: Date) => + date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric" + }); + + const createdOnDisplay = formatDate(createdOn); + const expiringOnDisplay = formatDate(expiringOn); + return ( @@ -30,7 +44,8 @@ export const ScimTokenExpiryNoticeTemplate = ({ ) : ( "One of your SCIM tokens" )}{" "} - for the organization {orgName} will expire within 24 hours. + for {orgName}, created on {createdOnDisplay}, is scheduled to expire on{" "} + {expiringOnDisplay}. If this token is still needed for your external platform sync, please create a new one before it expires to @@ -50,5 +65,7 @@ ScimTokenExpiryNoticeTemplate.PreviewProps = { orgName: "Example Organization", siteUrl: "https://infisical.com", url: "https://infisical.com", - tokenDescription: "Example SCIM Token" + tokenDescription: "Example SCIM Token", + createdOn: new Date("2025-11-27T00:00:00Z"), + expiringOn: new Date("2025-12-27T00:00:00Z") } as ScimTokenExpiryNoticeTemplateProps;