feat: adds expiring scim token notification

This commit is contained in:
Piyush Gupta
2025-11-27 01:49:33 +05:30
parent 46df44eeba
commit e7873282e6
11 changed files with 229 additions and 9 deletions
@@ -1,4 +1,5 @@
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-types";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { TKeyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
import { getConfig } from "@app/lib/config/env";
@@ -29,6 +30,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
orgService: TOrgServiceFactory;
userNotificationDAL: Pick<TUserNotificationDALFactory, "pruneNotifications">;
keyValueStoreDAL: Pick<TKeyValueStoreDALFactory, "pruneExpiredKeys">;
scimService: Pick<TScimServiceFactory, "notifyExpiringTokens">;
};
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
@@ -44,6 +46,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
secretVersionV2DAL,
identityUniversalAuthClientSecretDAL,
serviceTokenService,
scimService,
orgService,
userNotificationDAL,
keyValueStoreDAL
@@ -86,6 +89,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions();
await serviceTokenService.notifyExpiringTokens();
await scimService.notifyExpiringTokens();
await orgService.notifyInvitedUsers();
await auditLogDAL.pruneAuditLog();
await userNotificationDAL.pruneNotifications();
@@ -214,6 +214,8 @@ export const serviceTokenServiceFactory = ({
break;
}
const successfullyNotifiedTokenIds: string[] = [];
// eslint-disable-next-line no-await-in-loop
await Promise.all(
expiringTokens.map(async (token) => {
@@ -228,13 +230,19 @@ export const serviceTokenServiceFactory = ({
url: `${appCfg.SITE_URL}/organizations/${token.orgId}/projects/secret-management/${token.projectId}/access-management?selectedTab=service-tokens`
}
});
await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true });
successfullyNotifiedTokenIds.push(token.id);
} catch (error) {
logger.error(error, `Failed to send expiration notification for token ${token.id}:`);
}
})
);
// Batch update all successfully notified tokens in a single query
if (successfullyNotifiedTokenIds.length > 0) {
// eslint-disable-next-line no-await-in-loop
await serviceTokenDAL.update({ $in: { id: successfullyNotifiedTokenIds } }, { expiryNotificationSent: true });
}
processedCount += expiringTokens.length;
offset += batchSize;
}
@@ -0,0 +1,54 @@
import { Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ScimTokenExpiryNoticeTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
tokenDescription?: string;
orgName: string;
url: string;
}
export const ScimTokenExpiryNoticeTemplate = ({
tokenDescription,
siteUrl,
orgName,
url
}: ScimTokenExpiryNoticeTemplateProps) => {
return (
<BaseEmailWrapper title="SCIM Token Expiring Soon" preview="A SCIM token is about to expire." siteUrl={siteUrl}>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>SCIM token expiry notice</strong>
</Heading>
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">
{tokenDescription ? (
<>
Your SCIM token <strong>{tokenDescription}</strong>
</>
) : (
"One of your SCIM tokens"
)}{" "}
for the organization <strong>{orgName}</strong> will expire within 24 hours.
</Text>
<Text>
If this token is still needed for your external platform sync, please create a new one before it expires to
avoid disruption to your workflow.
</Text>
</Section>
<Section className="text-center">
<BaseButton href={url}>Manage SCIM Tokens</BaseButton>
</Section>
</BaseEmailWrapper>
);
};
export default ScimTokenExpiryNoticeTemplate;
ScimTokenExpiryNoticeTemplate.PreviewProps = {
orgName: "Example Organization",
siteUrl: "https://infisical.com",
url: "https://infisical.com",
tokenDescription: "Example SCIM Token"
} as ScimTokenExpiryNoticeTemplateProps;
@@ -19,6 +19,7 @@ export * from "./PasswordSetupTemplate";
export * from "./PkiExpirationAlertTemplate";
export * from "./ProjectAccessRequestTemplate";
export * from "./ProjectInvitationTemplate";
export * from "./ScimTokenExpiryNoticeTemplate";
export * from "./ScimUserProvisionedTemplate";
export * from "./SecretApprovalRequestBypassedTemplate";
export * from "./SecretApprovalRequestNeedsReviewTemplate";
@@ -28,6 +28,7 @@ import {
PkiExpirationAlertTemplate,
ProjectAccessRequestTemplate,
ProjectInvitationTemplate,
ScimTokenExpiryNoticeTemplate,
ScimUserProvisionedTemplate,
SecretApprovalRequestBypassedTemplate,
SecretApprovalRequestNeedsReviewTemplate,
@@ -74,6 +75,7 @@ export enum SmtpTemplates {
SecretLeakIncident = "secretLeakIncident",
WorkspaceInvite = "workspaceInvitation",
ScimUserProvisioned = "scimUserProvisioned",
ScimTokenExpired = "scimTokenExpired",
PkiExpirationAlert = "pkiExpirationAlert",
IntegrationSyncFailed = "integrationSyncFailed",
SecretSyncFailed = "secretSyncFailed",
@@ -121,6 +123,7 @@ const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
[SmtpTemplates.SecretLeakIncident]: SecretLeakIncidentTemplate,
[SmtpTemplates.WorkspaceInvite]: ProjectInvitationTemplate,
[SmtpTemplates.ScimUserProvisioned]: ScimUserProvisionedTemplate,
[SmtpTemplates.ScimTokenExpired]: ScimTokenExpiryNoticeTemplate,
[SmtpTemplates.SecretRequestCompleted]: SecretRequestCompletedTemplate,
[SmtpTemplates.UnlockAccount]: UnlockAccountTemplate,
[SmtpTemplates.ServiceTokenExpired]: ServiceTokenExpiryNoticeTemplate,