notifyExpiredTokens fixes

This commit is contained in:
carlosmonastyrski
2025-04-16 12:48:00 -03:00
parent 23a5a7a624
commit 965084cc0c
4 changed files with 70 additions and 68 deletions
@@ -22,7 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">; secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">; snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">; secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiredTokens">; serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiringTokens">;
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
}; };
@@ -53,7 +53,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await secretVersionDAL.pruneExcessVersions(); await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions();
await serviceTokenService.notifyExpiredTokens(); await serviceTokenService.notifyExpiringTokens();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
}); });
@@ -28,51 +28,36 @@ export const serviceTokenDALFactory = (db: TDbClient) => {
} }
}; };
const findExpiredTokens = async (tx?: Knex, batchSize = 1000) => { const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => {
try { try {
const allDocs: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] =
[]; await (tx || db.replicaNode())(TableName.ServiceToken)
let offset = 0; .leftJoin<TUsers>(
let hasMoreRecords = true; TableName.Users,
`${TableName.Users}.id`,
db.raw(`${TableName.ServiceToken}."createdBy"::uuid`)
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`)
.whereRaw(
`${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false`
)
.whereNotNull(`${TableName.Users}.email`)
.select(
db.ref("id").withSchema(TableName.ServiceToken),
db.ref("name").withSchema(TableName.ServiceToken),
db.ref("projectId").withSchema(TableName.ServiceToken),
db.ref("createdBy").withSchema(TableName.ServiceToken),
db.ref("email").withSchema(TableName.Users).as("createdByEmail"),
db.ref("name").withSchema(TableName.Project).as("projectName")
)
.limit(batchSize)
.offset(offset);
while (hasMoreRecords) { return batch;
// eslint-disable-next-line
const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] =
// eslint-disable-next-line no-await-in-loop
await (tx || db.replicaNode())(TableName.ServiceToken)
.leftJoin<TUsers>(
TableName.Users,
`${TableName.Users}.id`,
db.raw(`${TableName.ServiceToken}."createdBy"::uuid`)
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`)
.whereRaw(
`${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false`
)
.whereNotNull(`${TableName.Users}.email`)
.select(
db.ref("id").withSchema(TableName.ServiceToken),
db.ref("name").withSchema(TableName.ServiceToken),
db.ref("projectId").withSchema(TableName.ServiceToken),
db.ref("createdBy").withSchema(TableName.ServiceToken),
db.ref("email").withSchema(TableName.Users).as("createdByEmail"),
db.ref("name").withSchema(TableName.Project).as("projectName")
)
.limit(batchSize)
.offset(offset);
if (batch.length === 0) {
hasMoreRecords = false;
} else {
allDocs.push(...batch);
offset += batchSize;
}
}
return allDocs;
} catch (err) { } catch (err) {
throw new DatabaseError({ error: err, name: "FindExpiredTokens" }); throw new DatabaseError({ error: err, name: "FindExpiredTokens" });
} }
}; };
return { ...stOrm, findById, findExpiredTokens };
return { ...stOrm, findById, findExpiringTokens };
}; };
@@ -189,31 +189,48 @@ export const serviceTokenServiceFactory = ({
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
}; };
const notifyExpiredTokens = async () => { const notifyExpiringTokens = async () => {
const appCfg = getConfig(); const appCfg = getConfig();
let processedCount = 0;
let hasMoreRecords = true;
let offset = 0;
const batchSize = 500;
const expiredTokens = await serviceTokenDAL.findExpiredTokens(); while (hasMoreRecords) {
if (expiredTokens.length === 0) return; // eslint-disable-next-line no-await-in-loop
const expiringTokens = await serviceTokenDAL.findExpiringTokens(undefined, batchSize, offset);
await Promise.all( if (expiringTokens.length === 0) {
expiredTokens.map(async (token) => { hasMoreRecords = false;
try { break;
await smtpService.sendMail({ }
recipients: [token.createdByEmail],
subjectLine: "Your Service Token is about to expire", // eslint-disable-next-line no-await-in-loop
template: SmtpTemplates.ServiceTokenExpired, await Promise.all(
substitutions: { expiringTokens.map(async (token) => {
tokenName: token.name, try {
projectName: token.projectName, await smtpService.sendMail({
url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` recipients: [token.createdByEmail],
} subjectLine: "Service Token Expiry Notice",
}); template: SmtpTemplates.ServiceTokenExpired,
await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); substitutions: {
} catch (error) { tokenName: token.name,
logger.error(error, `Failed to send expiration notification for token ${token.id}:`); projectName: token.projectName,
} url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens`
}) }
); });
await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true });
} catch (error) {
logger.error(error, `Failed to send expiration notification for token ${token.id}:`);
}
})
);
processedCount += expiringTokens.length;
offset += batchSize;
}
return processedCount;
}; };
return { return {
@@ -222,6 +239,6 @@ export const serviceTokenServiceFactory = ({
getServiceToken, getServiceToken,
getProjectServiceTokens, getProjectServiceTokens,
fnValidateServiceToken, fnValidateServiceToken,
notifyExpiredTokens notifyExpiringTokens
}; };
}; };
@@ -7,10 +7,10 @@
</head> </head>
<body> <body>
<h2>Your Service Token is about to expire</h2> <h2>Service Token Expiry Notice</h2>
<p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p> <p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p>
<p>This token is currently being on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.</p> <p>This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.</p>
<a href="{{url}}">Create New Token</a> <a href="{{url}}">Create New Token</a>