From 23a5a7a6242b29c38fc0d781be6b6e5e7fe26d3f Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Tue, 15 Apr 2025 18:31:05 -0300 Subject: [PATCH] Improvements on notify expired service tokens --- ...701_add-notification-flag-service-token.ts | 10 ++-- backend/src/db/schemas/service-tokens.ts | 2 +- .../service-token/service-token-dal.ts | 59 +++++++++++++------ .../service-token/service-token-service.ts | 6 +- .../templates/serviceTokenExpired.handlebars | 8 +-- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts index dbfda96d1..9c5b4e730 100644 --- a/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts +++ b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts @@ -3,25 +3,25 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { - const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "notificationSent"); + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent"); if (!hasCol) { await knex.schema.alterTable(TableName.ServiceToken, (t) => { - t.boolean("notificationSent").defaultTo(false); + t.boolean("expiryNotificationSent").defaultTo(false); }); // Update only tokens where expiresAt is before current time await knex(TableName.ServiceToken) .whereRaw(`${TableName.ServiceToken}."expiresAt" < NOW()`) .whereNotNull("expiresAt") - .update({ notificationSent: true }); + .update({ expiryNotificationSent: true }); } } export async function down(knex: Knex): Promise { - const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "notificationSent"); + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent"); if (hasCol) { await knex.schema.alterTable(TableName.ServiceToken, (t) => { - t.dropColumn("notificationSent"); + t.dropColumn("expiryNotificationSent"); }); } } diff --git a/backend/src/db/schemas/service-tokens.ts b/backend/src/db/schemas/service-tokens.ts index 40859fc15..8ffddb10a 100644 --- a/backend/src/db/schemas/service-tokens.ts +++ b/backend/src/db/schemas/service-tokens.ts @@ -22,7 +22,7 @@ export const ServiceTokensSchema = z.object({ updatedAt: z.date(), createdBy: z.string(), projectId: z.string(), - notificationSent: z.boolean().default(false).nullable().optional() + expiryNotificationSent: z.boolean().default(false).nullable().optional() }); export type TServiceTokens = z.infer; diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index 188cd86cd..660d64a41 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -28,27 +28,48 @@ export const serviceTokenDALFactory = (db: TDbClient) => { } }; - const findExpiredTokens = async (tx?: Knex) => { + const findExpiredTokens = async (tx?: Knex, batchSize = 1000) => { try { - const docs: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = - await (tx || db.replicaNode())(TableName.ServiceToken) - .leftJoin( - 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() AND ${TableName.ServiceToken}."notificationSent" = false` - ) - .whereNotNull(`${TableName.Users}.email`) - .select(`${TableName.ServiceToken}.name`) - .select(`${TableName.ServiceToken}.id`) - .select(`${TableName.Project}.name as projectName`) - .select(`${TableName.ServiceToken}.projectId`) - .select(`${TableName.Users}.email as createdByEmail`); + const allDocs: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = + []; + let offset = 0; + let hasMoreRecords = true; - return docs; + while (hasMoreRecords) { + // 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( + 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) { throw new DatabaseError({ error: err, name: "FindExpiredTokens" }); } diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index db44e6272..760ad4b79 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -12,6 +12,7 @@ import { } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { ActorType } from "../auth/auth-type"; @@ -26,7 +27,6 @@ import { TGetServiceTokenInfoDTO, TProjectServiceTokensDTO } from "./service-token-types"; -import { logger } from "@app/lib/logger"; type TServiceTokenServiceFactoryDep = { serviceTokenDAL: TServiceTokenDALFactory; @@ -200,7 +200,7 @@ export const serviceTokenServiceFactory = ({ try { await smtpService.sendMail({ recipients: [token.createdByEmail], - subjectLine: "Service Token Expired", + subjectLine: "Your Service Token is about to expire", template: SmtpTemplates.ServiceTokenExpired, substitutions: { tokenName: token.name, @@ -208,7 +208,7 @@ export const serviceTokenServiceFactory = ({ url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` } }); - await serviceTokenDAL.update({ id: token.id }, { notificationSent: true }); + await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); } catch (error) { logger.error(error, `Failed to send expiration notification for token ${token.id}:`); } diff --git a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars index c0d364430..0b3c63dfb 100644 --- a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars +++ b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars @@ -3,14 +3,14 @@ - Service Token Expired + Service Token Expiring Soon -

Your Service Token has expired

-

Your service token "{{tokenName}}" has expired.

+

Your Service Token is about to expire

+

Your service token "{{tokenName}}" will expire within 24 hours.

-

This token was being used to access the project "{{projectName}}". To ensure continued service, please create a new token as soon as possible.

+

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.

Create New Token