mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 13:27:22 +00:00
Merge pull request #4953 from Infisical/feat/adds-expiring-scim-token-notification
feat: adds expiring scim token notification
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent");
|
||||||
|
if (!hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.ScimToken, (t) => {
|
||||||
|
t.boolean("expiryNotificationSent").defaultTo(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent");
|
||||||
|
if (hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.ScimToken, (t) => {
|
||||||
|
t.dropColumn("expiryNotificationSent");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ export const ScimTokensSchema = z.object({
|
|||||||
description: z.string(),
|
description: z.string(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
expiryNotificationSent: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TScimTokens = z.infer<typeof ScimTokensSchema>;
|
export type TScimTokens = z.infer<typeof ScimTokensSchema>;
|
||||||
|
|||||||
@@ -1,10 +1,56 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||||
import { ormify, TOrmify } from "@app/lib/knex";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TScimDALFactory = TOrmify<TableName.ScimToken>;
|
import { TExpiringScimToken } from "./scim-types";
|
||||||
|
|
||||||
export const scimDALFactory = (db: TDbClient): TScimDALFactory => {
|
export type TScimDALFactory = ReturnType<typeof scimDALFactory>;
|
||||||
|
|
||||||
|
export const scimDALFactory = (db: TDbClient) => {
|
||||||
const scimTokenOrm = ormify(db, TableName.ScimToken);
|
const scimTokenOrm = ormify(db, TableName.ScimToken);
|
||||||
return scimTokenOrm;
|
|
||||||
|
const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0): Promise<TExpiringScimToken[]> => {
|
||||||
|
try {
|
||||||
|
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 '7 days' AND
|
||||||
|
(${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") > NOW())
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.where(`${TableName.ScimToken}.expiryNotificationSent`, false)
|
||||||
|
.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<TExpiringScimToken[]>([
|
||||||
|
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);
|
||||||
|
|
||||||
|
return batch;
|
||||||
|
} catch (err) {
|
||||||
|
throw new DatabaseError({ error: err, name: "FindExpiringTokens" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...scimTokenOrm, findExpiringTokens };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto";
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal";
|
import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal";
|
||||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
@@ -47,7 +48,7 @@ import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, p
|
|||||||
import { TScimGroup, TScimServiceFactory } from "./scim-types";
|
import { TScimGroup, TScimServiceFactory } from "./scim-types";
|
||||||
|
|
||||||
type TScimServiceFactoryDep = {
|
type TScimServiceFactoryDep = {
|
||||||
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
|
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById" | "findExpiringTokens" | "update">;
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
TUserDALFactory,
|
TUserDALFactory,
|
||||||
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
|
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
|
||||||
@@ -1237,6 +1238,70 @@ export const scimServiceFactory = ({
|
|||||||
return { scimTokenId: scimToken.id, orgId: scimToken.orgId };
|
return { scimTokenId: scimToken.id, orgId: scimToken.orgId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const notifyExpiringTokens: TScimServiceFactory["notifyExpiringTokens"] = async () => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
let processedCount = 0;
|
||||||
|
let hasMoreRecords = true;
|
||||||
|
let offset = 0;
|
||||||
|
const batchSize = 500;
|
||||||
|
|
||||||
|
while (hasMoreRecords) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const expiringTokens = await scimDAL.findExpiringTokens(undefined, batchSize, offset);
|
||||||
|
|
||||||
|
if (expiringTokens.length === 0) {
|
||||||
|
hasMoreRecords = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfullyNotifiedTokenIds: string[] = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await Promise.all(
|
||||||
|
expiringTokens.map(async (token) => {
|
||||||
|
try {
|
||||||
|
if (token.adminEmails.length === 0) {
|
||||||
|
// Still mark as notified to avoid repeated checks
|
||||||
|
successfullyNotifiedTokenIds.push(token.id);
|
||||||
|
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",
|
||||||
|
template: SmtpTemplates.ScimTokenExpired,
|
||||||
|
substitutions: {
|
||||||
|
tokenDescription: token.description,
|
||||||
|
orgName: token.orgName,
|
||||||
|
url: `${appCfg.SITE_URL}/organizations/${token.orgId}/settings?selectedTab=provisioning-settings`,
|
||||||
|
createdOn,
|
||||||
|
expiringOn
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
successfullyNotifiedTokenIds.push(token.id);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Failed to send expiration notification for SCIM 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 scimDAL.update({ $in: { id: successfullyNotifiedTokenIds } }, { expiryNotificationSent: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
processedCount += expiringTokens.length;
|
||||||
|
offset += batchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedCount;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createScimToken,
|
createScimToken,
|
||||||
listScimTokens,
|
listScimTokens,
|
||||||
@@ -1253,6 +1318,7 @@ export const scimServiceFactory = ({
|
|||||||
deleteScimGroup,
|
deleteScimGroup,
|
||||||
replaceScimGroup,
|
replaceScimGroup,
|
||||||
updateScimGroup,
|
updateScimGroup,
|
||||||
fnValidateScimToken
|
fnValidateScimToken,
|
||||||
|
notifyExpiringTokens
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -158,6 +158,16 @@ export type TScimGroup = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TExpiringScimToken = {
|
||||||
|
id: string;
|
||||||
|
ttlDays: number;
|
||||||
|
description: string;
|
||||||
|
orgId: string;
|
||||||
|
createdAt: Date;
|
||||||
|
orgName: string;
|
||||||
|
adminEmails: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type TScimServiceFactory = {
|
export type TScimServiceFactory = {
|
||||||
createScimToken: (arg: TCreateScimTokenDTO) => Promise<{
|
createScimToken: (arg: TCreateScimTokenDTO) => Promise<{
|
||||||
scimToken: string;
|
scimToken: string;
|
||||||
@@ -200,4 +210,5 @@ export type TScimServiceFactory = {
|
|||||||
scimTokenId: string;
|
scimTokenId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
}>;
|
}>;
|
||||||
|
notifyExpiringTokens: () => Promise<number>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1913,6 +1913,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
// DAILY
|
// DAILY
|
||||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||||
|
scimService,
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
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 { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||||
import { TKeyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
|
import { TKeyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@@ -29,6 +30,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
|||||||
orgService: TOrgServiceFactory;
|
orgService: TOrgServiceFactory;
|
||||||
userNotificationDAL: Pick<TUserNotificationDALFactory, "pruneNotifications">;
|
userNotificationDAL: Pick<TUserNotificationDALFactory, "pruneNotifications">;
|
||||||
keyValueStoreDAL: Pick<TKeyValueStoreDALFactory, "pruneExpiredKeys">;
|
keyValueStoreDAL: Pick<TKeyValueStoreDALFactory, "pruneExpiredKeys">;
|
||||||
|
scimService: Pick<TScimServiceFactory, "notifyExpiringTokens">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
||||||
@@ -44,6 +46,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
secretVersionV2DAL,
|
secretVersionV2DAL,
|
||||||
identityUniversalAuthClientSecretDAL,
|
identityUniversalAuthClientSecretDAL,
|
||||||
serviceTokenService,
|
serviceTokenService,
|
||||||
|
scimService,
|
||||||
orgService,
|
orgService,
|
||||||
userNotificationDAL,
|
userNotificationDAL,
|
||||||
keyValueStoreDAL
|
keyValueStoreDAL
|
||||||
@@ -86,6 +89,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
await secretVersionV2DAL.pruneExcessVersions();
|
await secretVersionV2DAL.pruneExcessVersions();
|
||||||
await secretFolderVersionDAL.pruneExcessVersions();
|
await secretFolderVersionDAL.pruneExcessVersions();
|
||||||
await serviceTokenService.notifyExpiringTokens();
|
await serviceTokenService.notifyExpiringTokens();
|
||||||
|
await scimService.notifyExpiringTokens();
|
||||||
await orgService.notifyInvitedUsers();
|
await orgService.notifyInvitedUsers();
|
||||||
await auditLogDAL.pruneAuditLog();
|
await auditLogDAL.pruneAuditLog();
|
||||||
await userNotificationDAL.pruneNotifications();
|
await userNotificationDAL.pruneNotifications();
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ export const serviceTokenServiceFactory = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const successfullyNotifiedTokenIds: string[] = [];
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
expiringTokens.map(async (token) => {
|
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`
|
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) {
|
} catch (error) {
|
||||||
logger.error(error, `Failed to send expiration notification for token ${token.id}:`);
|
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;
|
processedCount += expiringTokens.length;
|
||||||
offset += batchSize;
|
offset += batchSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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;
|
||||||
|
createdOn: Date;
|
||||||
|
expiringOn: Date;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScimTokenExpiryNoticeTemplate = ({
|
||||||
|
tokenDescription,
|
||||||
|
siteUrl,
|
||||||
|
orgName,
|
||||||
|
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 (
|
||||||
|
<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 <strong>{orgName}</strong>, created on <strong>{createdOnDisplay}</strong>, is scheduled to expire on{" "}
|
||||||
|
<strong>{expiringOnDisplay}</strong>.
|
||||||
|
</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",
|
||||||
|
createdOn: new Date("2025-11-27T00:00:00Z"),
|
||||||
|
expiringOn: new Date("2025-12-27T00:00:00Z")
|
||||||
|
} as ScimTokenExpiryNoticeTemplateProps;
|
||||||
@@ -19,6 +19,7 @@ export * from "./PasswordSetupTemplate";
|
|||||||
export * from "./PkiExpirationAlertTemplate";
|
export * from "./PkiExpirationAlertTemplate";
|
||||||
export * from "./ProjectAccessRequestTemplate";
|
export * from "./ProjectAccessRequestTemplate";
|
||||||
export * from "./ProjectInvitationTemplate";
|
export * from "./ProjectInvitationTemplate";
|
||||||
|
export * from "./ScimTokenExpiryNoticeTemplate";
|
||||||
export * from "./ScimUserProvisionedTemplate";
|
export * from "./ScimUserProvisionedTemplate";
|
||||||
export * from "./SecretApprovalRequestBypassedTemplate";
|
export * from "./SecretApprovalRequestBypassedTemplate";
|
||||||
export * from "./SecretApprovalRequestNeedsReviewTemplate";
|
export * from "./SecretApprovalRequestNeedsReviewTemplate";
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
PkiExpirationAlertTemplate,
|
PkiExpirationAlertTemplate,
|
||||||
ProjectAccessRequestTemplate,
|
ProjectAccessRequestTemplate,
|
||||||
ProjectInvitationTemplate,
|
ProjectInvitationTemplate,
|
||||||
|
ScimTokenExpiryNoticeTemplate,
|
||||||
ScimUserProvisionedTemplate,
|
ScimUserProvisionedTemplate,
|
||||||
SecretApprovalRequestBypassedTemplate,
|
SecretApprovalRequestBypassedTemplate,
|
||||||
SecretApprovalRequestNeedsReviewTemplate,
|
SecretApprovalRequestNeedsReviewTemplate,
|
||||||
@@ -75,6 +76,7 @@ export enum SmtpTemplates {
|
|||||||
SecretLeakIncident = "secretLeakIncident",
|
SecretLeakIncident = "secretLeakIncident",
|
||||||
WorkspaceInvite = "workspaceInvitation",
|
WorkspaceInvite = "workspaceInvitation",
|
||||||
ScimUserProvisioned = "scimUserProvisioned",
|
ScimUserProvisioned = "scimUserProvisioned",
|
||||||
|
ScimTokenExpired = "scimTokenExpired",
|
||||||
PkiExpirationAlert = "pkiExpirationAlert",
|
PkiExpirationAlert = "pkiExpirationAlert",
|
||||||
IntegrationSyncFailed = "integrationSyncFailed",
|
IntegrationSyncFailed = "integrationSyncFailed",
|
||||||
SecretSyncFailed = "secretSyncFailed",
|
SecretSyncFailed = "secretSyncFailed",
|
||||||
@@ -123,6 +125,7 @@ const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
|
|||||||
[SmtpTemplates.SecretLeakIncident]: SecretLeakIncidentTemplate,
|
[SmtpTemplates.SecretLeakIncident]: SecretLeakIncidentTemplate,
|
||||||
[SmtpTemplates.WorkspaceInvite]: ProjectInvitationTemplate,
|
[SmtpTemplates.WorkspaceInvite]: ProjectInvitationTemplate,
|
||||||
[SmtpTemplates.ScimUserProvisioned]: ScimUserProvisionedTemplate,
|
[SmtpTemplates.ScimUserProvisioned]: ScimUserProvisionedTemplate,
|
||||||
|
[SmtpTemplates.ScimTokenExpired]: ScimTokenExpiryNoticeTemplate,
|
||||||
[SmtpTemplates.SecretRequestCompleted]: SecretRequestCompletedTemplate,
|
[SmtpTemplates.SecretRequestCompleted]: SecretRequestCompletedTemplate,
|
||||||
[SmtpTemplates.UnlockAccount]: UnlockAccountTemplate,
|
[SmtpTemplates.UnlockAccount]: UnlockAccountTemplate,
|
||||||
[SmtpTemplates.ServiceTokenExpired]: ServiceTokenExpiryNoticeTemplate,
|
[SmtpTemplates.ServiceTokenExpired]: ServiceTokenExpiryNoticeTemplate,
|
||||||
|
|||||||
Reference in New Issue
Block a user