mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-15 09:42:14 +00:00
Compare commits
8 Commits
commit-ui-
...
feat/remin
Author | SHA1 | Date | |
---|---|---|---|
bd4968b60d | |||
6449699f03 | |||
0e680e366b | |||
0af00ce82d | |||
3153450dc5 | |||
50ba2e543c | |||
e2559f10bc | |||
0efc314f33 |
@ -24,6 +24,7 @@ export const mockQueue = (): TQueueServiceFactory => {
|
|||||||
events[name] = event;
|
events[name] = event;
|
||||||
},
|
},
|
||||||
getRepeatableJobs: async () => [],
|
getRepeatableJobs: async () => [],
|
||||||
|
getDelayedJobs: async () => [],
|
||||||
clearQueue: async () => {},
|
clearQueue: async () => {},
|
||||||
stopJobById: async () => {},
|
stopJobById: async () => {},
|
||||||
stopJobByIdPg: async () => {},
|
stopJobByIdPg: async () => {},
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -93,6 +93,7 @@ import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env
|
|||||||
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||||
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
|
import { TReminderServiceFactory } from "@app/services/reminder/reminder-types";
|
||||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
||||||
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
@ -285,6 +286,7 @@ declare module "fastify" {
|
|||||||
secretScanningV2: TSecretScanningV2ServiceFactory;
|
secretScanningV2: TSecretScanningV2ServiceFactory;
|
||||||
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
|
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
|
||||||
pkiTemplate: TPkiTemplatesServiceFactory;
|
pkiTemplate: TPkiTemplatesServiceFactory;
|
||||||
|
reminder: TReminderServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
12
backend/src/@types/knex.d.ts
vendored
12
backend/src/@types/knex.d.ts
vendored
@ -504,6 +504,12 @@ import {
|
|||||||
TProjectMicrosoftTeamsConfigsInsert,
|
TProjectMicrosoftTeamsConfigsInsert,
|
||||||
TProjectMicrosoftTeamsConfigsUpdate
|
TProjectMicrosoftTeamsConfigsUpdate
|
||||||
} from "@app/db/schemas/project-microsoft-teams-configs";
|
} from "@app/db/schemas/project-microsoft-teams-configs";
|
||||||
|
import { TReminders, TRemindersInsert, TRemindersUpdate } from "@app/db/schemas/reminders";
|
||||||
|
import {
|
||||||
|
TRemindersRecipients,
|
||||||
|
TRemindersRecipientsInsert,
|
||||||
|
TRemindersRecipientsUpdate
|
||||||
|
} from "@app/db/schemas/reminders-recipients";
|
||||||
import {
|
import {
|
||||||
TSecretReminderRecipients,
|
TSecretReminderRecipients,
|
||||||
TSecretReminderRecipientsInsert,
|
TSecretReminderRecipientsInsert,
|
||||||
@ -1211,5 +1217,11 @@ declare module "knex/types/tables" {
|
|||||||
TSecretScanningConfigsInsert,
|
TSecretScanningConfigsInsert,
|
||||||
TSecretScanningConfigsUpdate
|
TSecretScanningConfigsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.Reminder]: KnexOriginal.CompositeTableType<TReminders, TRemindersInsert, TRemindersUpdate>;
|
||||||
|
[TableName.ReminderRecipient]: KnexOriginal.CompositeTableType<
|
||||||
|
TRemindersRecipients,
|
||||||
|
TRemindersRecipientsInsert,
|
||||||
|
TRemindersRecipientsUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Reminder))) {
|
||||||
|
await knex.schema.createTable(TableName.Reminder, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("secretId").nullable();
|
||||||
|
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
|
||||||
|
t.string("message", 1024).nullable();
|
||||||
|
t.integer("repeatDays").checkPositive().nullable();
|
||||||
|
t.timestamp("nextReminderDate").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.unique("secretId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ReminderRecipient))) {
|
||||||
|
await knex.schema.createTable(TableName.ReminderRecipient, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("reminderId").notNullable();
|
||||||
|
t.foreign("reminderId").references("id").inTable(TableName.Reminder).onDelete("CASCADE");
|
||||||
|
t.uuid("userId").notNullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.index("reminderId");
|
||||||
|
t.index("userId");
|
||||||
|
t.unique(["reminderId", "userId"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Reminder);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ReminderRecipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Reminder);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ReminderRecipient);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ReminderRecipient);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Reminder);
|
||||||
|
}
|
@ -172,7 +172,10 @@ export enum TableName {
|
|||||||
SecretScanningResource = "secret_scanning_resources",
|
SecretScanningResource = "secret_scanning_resources",
|
||||||
SecretScanningScan = "secret_scanning_scans",
|
SecretScanningScan = "secret_scanning_scans",
|
||||||
SecretScanningFinding = "secret_scanning_findings",
|
SecretScanningFinding = "secret_scanning_findings",
|
||||||
SecretScanningConfig = "secret_scanning_configs"
|
SecretScanningConfig = "secret_scanning_configs",
|
||||||
|
// reminders
|
||||||
|
Reminder = "reminders",
|
||||||
|
ReminderRecipient = "reminders_recipients"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
|
||||||
|
20
backend/src/db/schemas/reminders-recipients.ts
Normal file
20
backend/src/db/schemas/reminders-recipients.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const RemindersRecipientsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
reminderId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TRemindersRecipients = z.infer<typeof RemindersRecipientsSchema>;
|
||||||
|
export type TRemindersRecipientsInsert = Omit<z.input<typeof RemindersRecipientsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TRemindersRecipientsUpdate = Partial<Omit<z.input<typeof RemindersRecipientsSchema>, TImmutableDBKeys>>;
|
22
backend/src/db/schemas/reminders.ts
Normal file
22
backend/src/db/schemas/reminders.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const RemindersSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
secretId: z.string().uuid().nullable().optional(),
|
||||||
|
message: z.string().nullable().optional(),
|
||||||
|
repeatDays: z.number().nullable().optional(),
|
||||||
|
nextReminderDate: z.date(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TReminders = z.infer<typeof RemindersSchema>;
|
||||||
|
export type TRemindersInsert = Omit<z.input<typeof RemindersSchema>, TImmutableDBKeys>;
|
||||||
|
export type TRemindersUpdate = Partial<Omit<z.input<typeof RemindersSchema>, TImmutableDBKeys>>;
|
@ -467,7 +467,11 @@ export enum EventType {
|
|||||||
|
|
||||||
CREATE_PROJECT = "create-project",
|
CREATE_PROJECT = "create-project",
|
||||||
UPDATE_PROJECT = "update-project",
|
UPDATE_PROJECT = "update-project",
|
||||||
DELETE_PROJECT = "delete-project"
|
DELETE_PROJECT = "delete-project",
|
||||||
|
|
||||||
|
CREATE_SECRET_REMINDER = "create-secret-reminder",
|
||||||
|
GET_SECRET_REMINDER = "get-secret-reminder",
|
||||||
|
DELETE_SECRET_REMINDER = "delete-secret-reminder"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterableSecretEvents: EventType[] = [
|
export const filterableSecretEvents: EventType[] = [
|
||||||
@ -3312,6 +3316,31 @@ interface SecretScanningConfigUpdateEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecretReminderCreateEvent {
|
||||||
|
type: EventType.CREATE_SECRET_REMINDER;
|
||||||
|
metadata: {
|
||||||
|
secretId: string;
|
||||||
|
message?: string | null;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
nextReminderDate?: string | null;
|
||||||
|
recipients?: string[] | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecretReminderGetEvent {
|
||||||
|
type: EventType.GET_SECRET_REMINDER;
|
||||||
|
metadata: {
|
||||||
|
secretId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecretReminderDeleteEvent {
|
||||||
|
type: EventType.DELETE_SECRET_REMINDER;
|
||||||
|
metadata: {
|
||||||
|
secretId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SecretScanningConfigReadEvent {
|
interface SecretScanningConfigReadEvent {
|
||||||
type: EventType.SECRET_SCANNING_CONFIG_GET;
|
type: EventType.SECRET_SCANNING_CONFIG_GET;
|
||||||
metadata?: Record<string, never>; // not needed, based off projectId
|
metadata?: Record<string, never>; // not needed, based off projectId
|
||||||
@ -3674,4 +3703,7 @@ export type Event =
|
|||||||
| OrgUpdateEvent
|
| OrgUpdateEvent
|
||||||
| ProjectCreateEvent
|
| ProjectCreateEvent
|
||||||
| ProjectUpdateEvent
|
| ProjectUpdateEvent
|
||||||
| ProjectDeleteEvent;
|
| ProjectDeleteEvent
|
||||||
|
| SecretReminderCreateEvent
|
||||||
|
| SecretReminderGetEvent
|
||||||
|
| SecretReminderDeleteEvent;
|
||||||
|
@ -63,7 +63,9 @@ export enum QueueName {
|
|||||||
FolderTreeCheckpoint = "folder-tree-checkpoint",
|
FolderTreeCheckpoint = "folder-tree-checkpoint",
|
||||||
InvalidateCache = "invalidate-cache",
|
InvalidateCache = "invalidate-cache",
|
||||||
SecretScanningV2 = "secret-scanning-v2",
|
SecretScanningV2 = "secret-scanning-v2",
|
||||||
TelemetryAggregatedEvents = "telemetry-aggregated-events"
|
TelemetryAggregatedEvents = "telemetry-aggregated-events",
|
||||||
|
DailyReminders = "daily-reminders",
|
||||||
|
SecretReminderMigration = "secret-reminder-migration"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@ -103,7 +105,9 @@ export enum QueueJobs {
|
|||||||
SecretScanningV2SendNotification = "secret-scanning-v2-notification",
|
SecretScanningV2SendNotification = "secret-scanning-v2-notification",
|
||||||
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
|
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
|
||||||
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal",
|
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal",
|
||||||
TelemetryAggregatedEvents = "telemetry-aggregated-events"
|
TelemetryAggregatedEvents = "telemetry-aggregated-events",
|
||||||
|
DailyReminders = "daily-reminders",
|
||||||
|
SecretReminderMigration = "secret-reminder-migration"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@ -290,6 +294,14 @@ export type TQueueJobTypes = {
|
|||||||
caType: CaType;
|
caType: CaType;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
[QueueName.DailyReminders]: {
|
||||||
|
name: QueueJobs.DailyReminders;
|
||||||
|
payload: undefined;
|
||||||
|
};
|
||||||
|
[QueueName.SecretReminderMigration]: {
|
||||||
|
name: QueueJobs.SecretReminderMigration;
|
||||||
|
payload: undefined;
|
||||||
|
};
|
||||||
[QueueName.PkiSubscriber]: {
|
[QueueName.PkiSubscriber]: {
|
||||||
name: QueueJobs.PkiSubscriberDailyAutoRenewal;
|
name: QueueJobs.PkiSubscriberDailyAutoRenewal;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
@ -389,6 +401,11 @@ export type TQueueServiceFactory = {
|
|||||||
startOffset?: number,
|
startOffset?: number,
|
||||||
endOffset?: number
|
endOffset?: number
|
||||||
) => Promise<{ key: string; name: string; id: string | null }[]>;
|
) => Promise<{ key: string; name: string; id: string | null }[]>;
|
||||||
|
getDelayedJobs: (
|
||||||
|
name: QueueName,
|
||||||
|
startOffset?: number,
|
||||||
|
endOffset?: number
|
||||||
|
) => Promise<{ delay: number; timestamp: number; repeatJobKey?: string; data?: unknown }[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const queueServiceFactory = (
|
export const queueServiceFactory = (
|
||||||
@ -535,6 +552,13 @@ export const queueServiceFactory = (
|
|||||||
return q.getRepeatableJobs(startOffset, endOffset);
|
return q.getRepeatableJobs(startOffset, endOffset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDelayedJobs: TQueueServiceFactory["getDelayedJobs"] = (name, startOffset, endOffset) => {
|
||||||
|
const q = queueContainer[name];
|
||||||
|
if (!q) throw new Error(`Queue '${name}' not initialized`);
|
||||||
|
|
||||||
|
return q.getDelayed(startOffset, endOffset);
|
||||||
|
};
|
||||||
|
|
||||||
const stopRepeatableJobByJobId: TQueueServiceFactory["stopRepeatableJobByJobId"] = async (name, jobId) => {
|
const stopRepeatableJobByJobId: TQueueServiceFactory["stopRepeatableJobByJobId"] = async (name, jobId) => {
|
||||||
const q = queueContainer[name];
|
const q = queueContainer[name];
|
||||||
const job = await q.getJob(jobId);
|
const job = await q.getJob(jobId);
|
||||||
@ -581,6 +605,7 @@ export const queueServiceFactory = (
|
|||||||
stopJobById,
|
stopJobById,
|
||||||
stopJobByIdPg,
|
stopJobByIdPg,
|
||||||
getRepeatableJobs,
|
getRepeatableJobs,
|
||||||
|
getDelayedJobs,
|
||||||
startPg,
|
startPg,
|
||||||
queuePg,
|
queuePg,
|
||||||
schedulePg
|
schedulePg
|
||||||
|
@ -245,6 +245,10 @@ import { projectMembershipServiceFactory } from "@app/services/project-membershi
|
|||||||
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
||||||
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||||
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
|
import { reminderDALFactory } from "@app/services/reminder/reminder-dal";
|
||||||
|
import { dailyReminderQueueServiceFactory } from "@app/services/reminder/reminder-queue";
|
||||||
|
import { reminderServiceFactory } from "@app/services/reminder/reminder-service";
|
||||||
|
import { reminderRecipientDALFactory } from "@app/services/reminder-recipients/reminder-recipient-dal";
|
||||||
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
||||||
import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||||
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
@ -369,6 +373,9 @@ export const registerRoutes = async (
|
|||||||
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
||||||
const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db);
|
const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db);
|
||||||
|
|
||||||
|
const reminderDAL = reminderDALFactory(db);
|
||||||
|
const reminderRecipientDAL = reminderRecipientDALFactory(db);
|
||||||
|
|
||||||
const integrationDAL = integrationDALFactory(db);
|
const integrationDAL = integrationDALFactory(db);
|
||||||
const integrationAuthDAL = integrationAuthDALFactory(db);
|
const integrationAuthDAL = integrationAuthDALFactory(db);
|
||||||
const webhookDAL = webhookDALFactory(db);
|
const webhookDAL = webhookDALFactory(db);
|
||||||
@ -732,9 +739,17 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
||||||
|
|
||||||
|
const reminderService = reminderServiceFactory({
|
||||||
|
reminderDAL,
|
||||||
|
reminderRecipientDAL,
|
||||||
|
smtpService,
|
||||||
|
projectMembershipDAL,
|
||||||
|
permissionService,
|
||||||
|
secretV2BridgeDAL
|
||||||
|
});
|
||||||
|
|
||||||
const orgService = orgServiceFactory({
|
const orgService = orgServiceFactory({
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
queueService,
|
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
@ -760,7 +775,8 @@ export const registerRoutes = async (
|
|||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
oidcConfigDAL,
|
oidcConfigDAL,
|
||||||
loginService,
|
loginService,
|
||||||
projectBotService
|
projectBotService,
|
||||||
|
reminderService
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
@ -1058,7 +1074,6 @@ export const registerRoutes = async (
|
|||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
orgDAL,
|
|
||||||
auditLogService,
|
auditLogService,
|
||||||
userDAL,
|
userDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
@ -1080,11 +1095,11 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
secretReminderRecipientsDAL,
|
|
||||||
orgService,
|
orgService,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
folderCommitService,
|
folderCommitService,
|
||||||
secretSyncQueue
|
secretSyncQueue,
|
||||||
|
reminderService
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
@ -1093,7 +1108,6 @@ export const registerRoutes = async (
|
|||||||
projectSshConfigDAL,
|
projectSshConfigDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
queueService,
|
|
||||||
projectQueue: projectQueueService,
|
projectQueue: projectQueueService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
@ -1130,7 +1144,8 @@ export const registerRoutes = async (
|
|||||||
microsoftTeamsIntegrationDAL,
|
microsoftTeamsIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService
|
smtpService,
|
||||||
|
reminderService
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
@ -1229,6 +1244,7 @@ export const registerRoutes = async (
|
|||||||
kmsService,
|
kmsService,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
reminderService,
|
||||||
keyStore
|
keyStore
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1282,7 +1298,8 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
secretV2BridgeService,
|
secretV2BridgeService,
|
||||||
secretApprovalRequestService,
|
secretApprovalRequestService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
reminderService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretSharingService = secretSharingServiceFactory({
|
const secretSharingService = secretSharingServiceFactory({
|
||||||
@ -1609,7 +1626,6 @@ export const registerRoutes = async (
|
|||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretDAL,
|
|
||||||
secretFolderVersionDAL: folderVersionDAL,
|
secretFolderVersionDAL: folderVersionDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
@ -1620,6 +1636,13 @@ export const registerRoutes = async (
|
|||||||
orgService
|
orgService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dailyReminderQueueService = dailyReminderQueueServiceFactory({
|
||||||
|
reminderService,
|
||||||
|
queueService,
|
||||||
|
secretDAL: secretV2BridgeDAL,
|
||||||
|
secretReminderRecipientsDAL
|
||||||
|
});
|
||||||
|
|
||||||
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
||||||
queueService,
|
queueService,
|
||||||
pkiAlertService
|
pkiAlertService
|
||||||
@ -1913,6 +1936,8 @@ export const registerRoutes = async (
|
|||||||
await telemetryQueue.startTelemetryCheck();
|
await telemetryQueue.startTelemetryCheck();
|
||||||
await telemetryQueue.startAggregatedEventsJob();
|
await telemetryQueue.startAggregatedEventsJob();
|
||||||
await dailyResourceCleanUp.startCleanUp();
|
await dailyResourceCleanUp.startCleanUp();
|
||||||
|
await dailyReminderQueueService.startDailyRemindersJob();
|
||||||
|
await dailyReminderQueueService.startSecretReminderMigrationJob();
|
||||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||||
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
||||||
await kmsService.startService();
|
await kmsService.startService();
|
||||||
@ -2023,7 +2048,8 @@ export const registerRoutes = async (
|
|||||||
assumePrivileges: assumePrivilegeService,
|
assumePrivileges: assumePrivilegeService,
|
||||||
githubOrgSync: githubOrgSyncConfigService,
|
githubOrgSync: githubOrgSyncConfigService,
|
||||||
folderCommit: folderCommitService,
|
folderCommit: folderCommitService,
|
||||||
secretScanningV2: secretScanningV2Service
|
secretScanningV2: secretScanningV2Service,
|
||||||
|
reminder: reminderService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
@ -42,6 +42,7 @@ import { registerProjectEnvRouter } from "./project-env-router";
|
|||||||
import { registerProjectKeyRouter } from "./project-key-router";
|
import { registerProjectKeyRouter } from "./project-key-router";
|
||||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
|
import { SECRET_REMINDER_REGISTER_ROUTER_MAP } from "./reminder-routers";
|
||||||
import { registerSecretFolderRouter } from "./secret-folder-router";
|
import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||||
import { registerSecretImportRouter } from "./secret-import-router";
|
import { registerSecretImportRouter } from "./secret-import-router";
|
||||||
import { registerSecretRequestsRouter } from "./secret-requests-router";
|
import { registerSecretRequestsRouter } from "./secret-requests-router";
|
||||||
@ -172,4 +173,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
{ prefix: "/secret-syncs" }
|
{ prefix: "/secret-syncs" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (reminderRouter) => {
|
||||||
|
// register service specific reminder endpoints (reminders/secret)
|
||||||
|
for await (const [reminderType, router] of Object.entries(SECRET_REMINDER_REGISTER_ROUTER_MAP)) {
|
||||||
|
await reminderRouter.register(router, { prefix: `/${reminderType}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prefix: "/reminders" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
8
backend/src/server/routes/v1/reminder-routers/index.ts
Normal file
8
backend/src/server/routes/v1/reminder-routers/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ReminderType } from "@app/services/reminder/reminder-enums";
|
||||||
|
|
||||||
|
import { registerSecretReminderRouter } from "./secret-reminder-router";
|
||||||
|
|
||||||
|
export const SECRET_REMINDER_REGISTER_ROUTER_MAP: Record<ReminderType, (server: FastifyZodProvider) => Promise<void>> =
|
||||||
|
{
|
||||||
|
[ReminderType.SECRETS]: registerSecretReminderRouter
|
||||||
|
};
|
@ -0,0 +1,154 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { RemindersSchema } from "@app/db/schemas/reminders";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSecretReminderRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/:secretId",
|
||||||
|
method: "POST",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
secretId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
message: z.string().trim().max(1024).optional(),
|
||||||
|
repeatDays: z.number().min(1).nullable().optional(),
|
||||||
|
nextReminderDate: z.string().datetime().nullable().optional(),
|
||||||
|
recipients: z.string().array().optional()
|
||||||
|
})
|
||||||
|
.refine((data) => {
|
||||||
|
return data.repeatDays || data.nextReminderDate;
|
||||||
|
}, "At least one of repeatDays or nextReminderDate is required"),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.reminder.createReminder({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
reminder: {
|
||||||
|
secretId: req.params.secretId,
|
||||||
|
message: req.body.message,
|
||||||
|
repeatDays: req.body.repeatDays,
|
||||||
|
nextReminderDate: req.body.nextReminderDate,
|
||||||
|
recipients: req.body.recipients
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SECRET_REMINDER,
|
||||||
|
metadata: {
|
||||||
|
secretId: req.params.secretId,
|
||||||
|
message: req.body.message,
|
||||||
|
repeatDays: req.body.repeatDays,
|
||||||
|
nextReminderDate: req.body.nextReminderDate,
|
||||||
|
recipients: req.body.recipients
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Successfully created reminder" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:secretId",
|
||||||
|
method: "GET",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
secretId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
reminder: RemindersSchema.extend({
|
||||||
|
recipients: z.string().array().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const reminder = await server.services.reminder.getReminder({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
secretId: req.params.secretId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_REMINDER,
|
||||||
|
metadata: {
|
||||||
|
secretId: req.params.secretId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { reminder };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:secretId",
|
||||||
|
method: "DELETE",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
secretId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.reminder.deleteReminder({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
secretId: req.params.secretId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SECRET_REMINDER,
|
||||||
|
metadata: {
|
||||||
|
secretId: req.params.secretId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { message: "Successfully deleted reminder" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -49,7 +49,7 @@ import { groupBy } from "@app/lib/fn";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
|
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
@ -67,6 +67,7 @@ import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
|||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
|
import { TReminderServiceFactory } from "../reminder/reminder-types";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@ -134,8 +135,8 @@ type TOrgServiceFactoryDep = {
|
|||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
|
||||||
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||||
|
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||||
@ -167,8 +168,8 @@ export const orgServiceFactory = ({
|
|||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
queueService,
|
loginService,
|
||||||
loginService
|
reminderService
|
||||||
}: TOrgServiceFactoryDep) => {
|
}: TOrgServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Get organization details by the organization id
|
* Get organization details by the organization id
|
||||||
@ -610,7 +611,7 @@ export const orgServiceFactory = ({
|
|||||||
await fnDeleteProjectSecretReminders(project.id, {
|
await fnDeleteProjectSecretReminders(project.id, {
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
queueService,
|
reminderService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
folderDAL
|
folderDAL
|
||||||
});
|
});
|
||||||
|
@ -32,7 +32,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
|
|||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
|
||||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
@ -60,6 +59,7 @@ import { TProjectMembershipDALFactory } from "../project-membership/project-memb
|
|||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||||
|
import { TReminderServiceFactory } from "../reminder/reminder-types";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@ -162,7 +162,6 @@ type TProjectServiceFactoryDep = {
|
|||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
|
||||||
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
@ -179,6 +178,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
| "createCipherPairWithDataKey"
|
| "createCipherPairWithDataKey"
|
||||||
>;
|
>;
|
||||||
projectTemplateService: TProjectTemplateServiceFactory;
|
projectTemplateService: TProjectTemplateServiceFactory;
|
||||||
|
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||||
@ -191,7 +191,6 @@ export const projectServiceFactory = ({
|
|||||||
projectQueue,
|
projectQueue,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
queueService,
|
|
||||||
projectBotService,
|
projectBotService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
@ -226,7 +225,8 @@ export const projectServiceFactory = ({
|
|||||||
microsoftTeamsIntegrationDAL,
|
microsoftTeamsIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService
|
smtpService,
|
||||||
|
reminderService
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
@ -555,7 +555,7 @@ export const projectServiceFactory = ({
|
|||||||
await fnDeleteProjectSecretReminders(project.id, {
|
await fnDeleteProjectSecretReminders(project.id, {
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
queueService,
|
reminderService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
folderDAL
|
folderDAL
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TReminderRecipientDALFactory = ReturnType<typeof reminderRecipientDALFactory>;
|
||||||
|
|
||||||
|
export const reminderRecipientDALFactory = (db: TDbClient) => {
|
||||||
|
const reminderRecipientOrm = ormify(db, TableName.ReminderRecipient);
|
||||||
|
|
||||||
|
return { ...reminderRecipientOrm };
|
||||||
|
};
|
133
backend/src/services/reminder/reminder-dal.ts
Normal file
133
backend/src/services/reminder/reminder-dal.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import {
|
||||||
|
TableName,
|
||||||
|
TOrganizations,
|
||||||
|
TProjectEnvironments,
|
||||||
|
TProjects,
|
||||||
|
TSecretFolders,
|
||||||
|
TSecretsV2,
|
||||||
|
TUsers
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { RemindersSchema } from "@app/db/schemas/reminders";
|
||||||
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TReminderDALFactory = ReturnType<typeof reminderDALFactory>;
|
||||||
|
|
||||||
|
export const reminderDALFactory = (db: TDbClient) => {
|
||||||
|
const reminderOrm = ormify(db, TableName.Reminder);
|
||||||
|
|
||||||
|
const getTodayDateRange = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getUTCFullYear();
|
||||||
|
const month = today.getUTCMonth();
|
||||||
|
const date = today.getUTCDate();
|
||||||
|
|
||||||
|
// Start of day: 00:00:00.000 UTC
|
||||||
|
const startOfDay = new Date(Date.UTC(year, month, date, 0, 0, 0, 0));
|
||||||
|
|
||||||
|
// End of day: 23:59:59.999 UTC
|
||||||
|
const endOfDay = new Date(Date.UTC(year, month, date, 23, 59, 59, 999));
|
||||||
|
|
||||||
|
return {
|
||||||
|
startOfDay,
|
||||||
|
endOfDay
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSecretDailyReminders = async (tx?: Knex) => {
|
||||||
|
const { startOfDay, endOfDay } = getTodayDateRange();
|
||||||
|
|
||||||
|
const rawReminders = await (tx || db)(TableName.Reminder)
|
||||||
|
.whereBetween("nextReminderDate", [startOfDay, endOfDay])
|
||||||
|
.leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`)
|
||||||
|
.leftJoin<TUsers>(TableName.Users, `${TableName.ReminderRecipient}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin<TSecretsV2>(TableName.SecretV2, `${TableName.Reminder}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
|
.leftJoin<TSecretFolders>(
|
||||||
|
TableName.SecretFolder,
|
||||||
|
`${TableName.SecretV2}.folderId`,
|
||||||
|
`${TableName.SecretFolder}.id`
|
||||||
|
)
|
||||||
|
.leftJoin<TProjectEnvironments>(
|
||||||
|
TableName.Environment,
|
||||||
|
`${TableName.SecretFolder}.envId`,
|
||||||
|
`${TableName.Environment}.id`
|
||||||
|
)
|
||||||
|
.leftJoin<TProjects>(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.leftJoin<TOrganizations>(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Reminder))
|
||||||
|
.select(db.ref("email").withSchema(TableName.Users))
|
||||||
|
.select(db.ref("name").withSchema(TableName.Project).as("projectName"))
|
||||||
|
.select(db.ref("id").withSchema(TableName.Project).as("projectId"))
|
||||||
|
.select(db.ref("name").withSchema(TableName.Organization).as("organizationName"));
|
||||||
|
|
||||||
|
const reminders = sqlNestRelationships({
|
||||||
|
data: rawReminders,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({
|
||||||
|
_id: el.id,
|
||||||
|
...RemindersSchema.parse(el),
|
||||||
|
projectName: el.projectName,
|
||||||
|
projectId: el.projectId,
|
||||||
|
organizationName: el.organizationName
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
label: "recipients" as const,
|
||||||
|
mapper: ({ email }) => ({
|
||||||
|
email
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return reminders;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findUpcomingReminders = async (daysAhead: number = 7, tx?: Knex) => {
|
||||||
|
const { startOfDay } = getTodayDateRange();
|
||||||
|
const futureDate = new Date(startOfDay);
|
||||||
|
futureDate.setDate(futureDate.getDate() + daysAhead);
|
||||||
|
|
||||||
|
const reminders = await (tx || db)(TableName.Reminder)
|
||||||
|
.where("nextReminderDate", ">=", startOfDay)
|
||||||
|
.where("nextReminderDate", "<=", futureDate)
|
||||||
|
.orderBy("nextReminderDate", "asc")
|
||||||
|
.leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`)
|
||||||
|
.select(selectAllTableCols(TableName.Reminder))
|
||||||
|
.select(db.ref("userId").withSchema(TableName.ReminderRecipient));
|
||||||
|
return reminders;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSecretReminder = async (secretId: string, tx?: Knex) => {
|
||||||
|
const rawReminders = await (tx || db)(TableName.Reminder)
|
||||||
|
.where(`${TableName.Reminder}.secretId`, secretId)
|
||||||
|
.leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`)
|
||||||
|
.select(selectAllTableCols(TableName.Reminder))
|
||||||
|
.select(db.ref("userId").withSchema(TableName.ReminderRecipient));
|
||||||
|
const reminders = sqlNestRelationships({
|
||||||
|
data: rawReminders,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({
|
||||||
|
_id: el.id,
|
||||||
|
...RemindersSchema.parse(el)
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "userId",
|
||||||
|
label: "recipients" as const,
|
||||||
|
mapper: ({ userId }) => userId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return reminders[0] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...reminderOrm,
|
||||||
|
findSecretDailyReminders,
|
||||||
|
findUpcomingReminders,
|
||||||
|
findSecretReminder
|
||||||
|
};
|
||||||
|
};
|
3
backend/src/services/reminder/reminder-enums.ts
Normal file
3
backend/src/services/reminder/reminder-enums.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export enum ReminderType {
|
||||||
|
SECRETS = "secrets"
|
||||||
|
}
|
196
backend/src/services/reminder/reminder-queue.ts
Normal file
196
backend/src/services/reminder/reminder-queue.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
|
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
|
||||||
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
|
import { TReminderServiceFactory } from "./reminder-types";
|
||||||
|
|
||||||
|
type TDailyReminderQueueServiceFactoryDep = {
|
||||||
|
reminderService: TReminderServiceFactory;
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "transaction" | "findSecretsWithReminderRecipients">;
|
||||||
|
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDailyReminderQueueServiceFactory = ReturnType<typeof dailyReminderQueueServiceFactory>;
|
||||||
|
|
||||||
|
const uuidRegex = new RE2(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
||||||
|
|
||||||
|
export const dailyReminderQueueServiceFactory = ({
|
||||||
|
reminderService,
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
secretReminderRecipientsDAL
|
||||||
|
}: TDailyReminderQueueServiceFactoryDep) => {
|
||||||
|
queueService.start(QueueName.DailyReminders, async () => {
|
||||||
|
logger.info(`${QueueName.DailyReminders}: queue task started`);
|
||||||
|
await reminderService.sendDailyReminders();
|
||||||
|
logger.info(`${QueueName.DailyReminders}: queue task completed`);
|
||||||
|
});
|
||||||
|
|
||||||
|
queueService.start(QueueName.SecretReminderMigration, async () => {
|
||||||
|
const REMINDER_PRUNE_BATCH_SIZE = 5_000;
|
||||||
|
const MAX_RETRY_ON_FAILURE = 3;
|
||||||
|
let numberOfRetryOnFailure = 0;
|
||||||
|
let deletedReminderCount = 0;
|
||||||
|
|
||||||
|
logger.info(`${QueueName.SecretReminderMigration}: queue task started`);
|
||||||
|
try {
|
||||||
|
const repeatableJobs = await queueService.getRepeatableJobs(QueueName.SecretReminder);
|
||||||
|
const delayedJobs = await queueService.getDelayedJobs(QueueName.SecretReminder);
|
||||||
|
logger.info(`${QueueName.SecretReminderMigration}: found ${repeatableJobs.length} secret reminder jobs`);
|
||||||
|
|
||||||
|
const reminderJobs = repeatableJobs
|
||||||
|
.map((job) => ({ secretId: job.id?.replace("reminder-", "") as string, jobKey: job.key }))
|
||||||
|
.filter(Boolean);
|
||||||
|
const reminderDelayedJobs = delayedJobs.reduce((map, job) => {
|
||||||
|
const match = uuidRegex.exec(job.repeatJobKey || "");
|
||||||
|
if (match) {
|
||||||
|
map.set(match[0], {
|
||||||
|
timestamp: job.timestamp,
|
||||||
|
delay: job.delay,
|
||||||
|
data: job.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, new Map<string, { timestamp: number; delay: number; data: unknown }>());
|
||||||
|
if (reminderJobs.length === 0) {
|
||||||
|
logger.info(`${QueueName.SecretReminderMigration}: no reminder jobs found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let offset = 0; offset < reminderJobs.length; offset += REMINDER_PRUNE_BATCH_SIZE) {
|
||||||
|
try {
|
||||||
|
const batch = reminderJobs.slice(offset, offset + REMINDER_PRUNE_BATCH_SIZE);
|
||||||
|
const batchIds = batch.map((job) => job.secretId);
|
||||||
|
|
||||||
|
// Find existing secrets with pagination
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const secrets = await secretDAL.findSecretsWithReminderRecipients(batchIds, REMINDER_PRUNE_BATCH_SIZE);
|
||||||
|
const secretsWithReminder = secrets.filter((secret) => secret.reminderRepeatDays);
|
||||||
|
|
||||||
|
const foundSecretIds = new Set(secretsWithReminder.map((secret) => secret.id));
|
||||||
|
|
||||||
|
// Find IDs that don't exist in either table
|
||||||
|
const secretIdsNotFound = batchIds.filter((secretId) => !foundSecretIds.has(secretId));
|
||||||
|
|
||||||
|
// Delete reminders for non-existent secrets
|
||||||
|
for (const secretId of secretIdsNotFound) {
|
||||||
|
const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey;
|
||||||
|
|
||||||
|
if (jobKey) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey);
|
||||||
|
deletedReminderCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const secretId of foundSecretIds) {
|
||||||
|
const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey;
|
||||||
|
|
||||||
|
if (jobKey) {
|
||||||
|
await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey);
|
||||||
|
deletedReminderCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await secretDAL.transaction(async (tx) => {
|
||||||
|
await reminderService.batchCreateReminders(
|
||||||
|
secretsWithReminder.map((secret) => {
|
||||||
|
const delayedJob = reminderDelayedJobs.get(secret.id);
|
||||||
|
const projectId = (delayedJob?.data as { projectId?: string })?.projectId;
|
||||||
|
const nextDate = delayedJob ? new Date(delayedJob.timestamp + delayedJob.delay) : undefined;
|
||||||
|
return {
|
||||||
|
secretId: secret.id,
|
||||||
|
message: secret.reminderNote,
|
||||||
|
repeatDays: secret.reminderRepeatDays,
|
||||||
|
nextReminderDate: nextDate,
|
||||||
|
recipients: secret.recipients || [],
|
||||||
|
projectId
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await secretReminderRecipientsDAL.delete({ $in: { secretId: secretsWithReminder.map((s) => s.id) } }, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
numberOfRetryOnFailure = 0;
|
||||||
|
} catch (error) {
|
||||||
|
numberOfRetryOnFailure += 1;
|
||||||
|
logger.error(error, `Failed to process batch at offset ${offset}`);
|
||||||
|
|
||||||
|
if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry the current batch
|
||||||
|
offset -= REMINDER_PRUNE_BATCH_SIZE;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500 * numberOfRetryOnFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between batches
|
||||||
|
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to complete secret reminder pruning");
|
||||||
|
} finally {
|
||||||
|
logger.info(
|
||||||
|
`${QueueName.SecretReminderMigration}: secret reminders completed. Deleted ${deletedReminderCount} reminders`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// we do a repeat cron job in utc timezone at 12 Midnight each day
|
||||||
|
const startDailyRemindersJob = async () => {
|
||||||
|
// clear previous job
|
||||||
|
await queueService.stopRepeatableJob(
|
||||||
|
QueueName.DailyReminders,
|
||||||
|
QueueJobs.DailyReminders,
|
||||||
|
{ pattern: "0 0 * * *", utc: true },
|
||||||
|
QueueName.DailyReminders // just a job id
|
||||||
|
);
|
||||||
|
|
||||||
|
await queueService.queue(QueueName.DailyReminders, QueueJobs.DailyReminders, undefined, {
|
||||||
|
delay: 5000,
|
||||||
|
jobId: QueueName.DailyReminders,
|
||||||
|
repeat: { pattern: "0 0 * * *", utc: true }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: remove once all the old reminders in queues are migrated
|
||||||
|
const startSecretReminderMigrationJob = async () => {
|
||||||
|
// clear previous job
|
||||||
|
await queueService.stopRepeatableJob(
|
||||||
|
QueueName.SecretReminderMigration,
|
||||||
|
QueueJobs.SecretReminderMigration,
|
||||||
|
{ pattern: "0 */1 * * *", utc: true },
|
||||||
|
QueueName.SecretReminderMigration // just a job id
|
||||||
|
);
|
||||||
|
|
||||||
|
await queueService.queue(QueueName.SecretReminderMigration, QueueJobs.SecretReminderMigration, undefined, {
|
||||||
|
delay: 5000,
|
||||||
|
jobId: QueueName.SecretReminderMigration,
|
||||||
|
repeat: { pattern: "0 */1 * * *", utc: true }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.listen(QueueName.DailyReminders, "failed", (_, err) => {
|
||||||
|
logger.error(err, `${QueueName.DailyReminders}: daily reminder processing failed`);
|
||||||
|
});
|
||||||
|
|
||||||
|
queueService.listen(QueueName.SecretReminderMigration, "failed", (_, err) => {
|
||||||
|
logger.error(err, `${QueueName.SecretReminderMigration}: secret reminder migration failed`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDailyRemindersJob,
|
||||||
|
startSecretReminderMigrationJob
|
||||||
|
};
|
||||||
|
};
|
356
backend/src/services/reminder/reminder-service.ts
Normal file
356
backend/src/services/reminder/reminder-service.ts
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TReminderRecipientDALFactory } from "../reminder-recipients/reminder-recipient-dal";
|
||||||
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TReminderDALFactory } from "./reminder-dal";
|
||||||
|
import { TBatchCreateReminderDTO, TCreateReminderDTO, TReminderServiceFactory } from "./reminder-types";
|
||||||
|
|
||||||
|
type TReminderServiceFactoryDep = {
|
||||||
|
reminderDAL: TReminderDALFactory;
|
||||||
|
reminderRecipientDAL: TReminderRecipientDALFactory;
|
||||||
|
smtpService: TSmtpService;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "invalidateSecretCacheByProjectId" | "findOneWithTags">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reminderServiceFactory = ({
|
||||||
|
reminderDAL,
|
||||||
|
reminderRecipientDAL,
|
||||||
|
smtpService,
|
||||||
|
projectMembershipDAL,
|
||||||
|
permissionService,
|
||||||
|
secretV2BridgeDAL
|
||||||
|
}: TReminderServiceFactoryDep): TReminderServiceFactory => {
|
||||||
|
const $addDays = (days: number, fromDate: Date = new Date()): Date => {
|
||||||
|
const result = new Date(fromDate);
|
||||||
|
result.setDate(result.getDate() + days);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise<void> => {
|
||||||
|
if (!newRecipients || newRecipients.length === 0) {
|
||||||
|
// If no recipients provided, remove all existing recipients
|
||||||
|
await reminderRecipientDAL.deleteById(reminderId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates from input
|
||||||
|
const uniqueRecipients = [...new Set(newRecipients)];
|
||||||
|
|
||||||
|
// Get existing recipients
|
||||||
|
const existingRecipients = await reminderRecipientDAL.find({ reminderId });
|
||||||
|
const existingUserIds = new Set(existingRecipients.map((r) => r.userId));
|
||||||
|
const newUserIds = new Set(uniqueRecipients);
|
||||||
|
|
||||||
|
// Find recipients to add and remove
|
||||||
|
const recipientsToAdd = uniqueRecipients.filter((userId) => !existingUserIds.has(userId));
|
||||||
|
const recipientsToRemove = existingRecipients.filter((r) => !newUserIds.has(r.userId));
|
||||||
|
|
||||||
|
// Perform database operations
|
||||||
|
if (recipientsToRemove.length > 0) {
|
||||||
|
await reminderRecipientDAL.delete({ $in: { id: recipientsToRemove.map((r) => r.id) } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipientsToAdd.length > 0) {
|
||||||
|
await reminderRecipientDAL.insertMany(
|
||||||
|
recipientsToAdd.map((userId) => ({
|
||||||
|
reminderId,
|
||||||
|
userId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createReminderInternal: TReminderServiceFactory["createReminderInternal"] = async ({
|
||||||
|
secretId,
|
||||||
|
message,
|
||||||
|
repeatDays,
|
||||||
|
nextReminderDate: nextReminderDateInput,
|
||||||
|
recipients,
|
||||||
|
projectId
|
||||||
|
}: {
|
||||||
|
secretId?: string;
|
||||||
|
message?: string | null;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
nextReminderDate?: string | null;
|
||||||
|
recipients?: string[] | null;
|
||||||
|
projectId: string;
|
||||||
|
}) => {
|
||||||
|
if (!secretId) {
|
||||||
|
throw new BadRequestError({ message: "secretId is required" });
|
||||||
|
}
|
||||||
|
let nextReminderDate;
|
||||||
|
if (nextReminderDateInput) {
|
||||||
|
nextReminderDate = new Date(nextReminderDateInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repeatDays && repeatDays > 0) {
|
||||||
|
nextReminderDate = $addDays(repeatDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextReminderDate) {
|
||||||
|
throw new BadRequestError({ message: "repeatDays must be a positive number" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingReminder = await reminderDAL.findOne({ secretId });
|
||||||
|
let reminderId: string;
|
||||||
|
|
||||||
|
if (existingReminder) {
|
||||||
|
// Update existing reminder
|
||||||
|
await reminderDAL.updateById(existingReminder.id, {
|
||||||
|
message,
|
||||||
|
repeatDays,
|
||||||
|
nextReminderDate
|
||||||
|
});
|
||||||
|
reminderId = existingReminder.id;
|
||||||
|
} else {
|
||||||
|
// Create new reminder
|
||||||
|
const newReminder = await reminderDAL.create({
|
||||||
|
secretId,
|
||||||
|
message,
|
||||||
|
repeatDays,
|
||||||
|
nextReminderDate
|
||||||
|
});
|
||||||
|
reminderId = newReminder.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage recipients (add/update/delete as needed)
|
||||||
|
await $manageReminderRecipients(reminderId, recipients);
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
|
return { id: reminderId, created: !existingReminder };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createReminder: TReminderServiceFactory["createReminder"] = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
reminder
|
||||||
|
}: TCreateReminderDTO) => {
|
||||||
|
const secret = await secretV2BridgeDAL.findOneWithTags({ [`${TableName.SecretV2}.id` as "id"]: reminder.secretId });
|
||||||
|
if (!secret) {
|
||||||
|
throw new BadRequestError({ message: `Secret ${reminder.secretId} not found` });
|
||||||
|
}
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: secret.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionSecretActions.Edit, ProjectPermissionSub.Secrets);
|
||||||
|
|
||||||
|
const response = await createReminderInternal({
|
||||||
|
...reminder,
|
||||||
|
projectId: secret.projectId
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReminder: TReminderServiceFactory["getReminder"] = async ({
|
||||||
|
secretId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: {
|
||||||
|
secretId: string;
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
}) => {
|
||||||
|
const secret = await secretV2BridgeDAL.findOneWithTags({ [`${TableName.SecretV2}.id` as "id"]: secretId });
|
||||||
|
if (!secret) {
|
||||||
|
throw new BadRequestError({ message: `Secret ${secretId} not found` });
|
||||||
|
}
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: secret.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretActions.DescribeSecret,
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
const reminder = await reminderDAL.findSecretReminder(secretId);
|
||||||
|
return reminder;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendDailyReminders: TReminderServiceFactory["sendDailyReminders"] = async () => {
|
||||||
|
const remindersToSend = await reminderDAL.findSecretDailyReminders();
|
||||||
|
|
||||||
|
for (const reminder of remindersToSend) {
|
||||||
|
try {
|
||||||
|
await reminderDAL.transaction(async (tx) => {
|
||||||
|
const recipients: string[] = reminder.recipients
|
||||||
|
.map((r) => r.email)
|
||||||
|
.filter((email): email is string => Boolean(email));
|
||||||
|
if (recipients.length === 0) {
|
||||||
|
const members = await projectMembershipDAL.findAllProjectMembers(reminder.projectId);
|
||||||
|
recipients.push(...members.map((m) => m.user.email).filter((email): email is string => Boolean(email)));
|
||||||
|
}
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.SecretReminder,
|
||||||
|
subjectLine: "Infisical secret reminder",
|
||||||
|
recipients,
|
||||||
|
substitutions: {
|
||||||
|
reminderNote: reminder.message || "",
|
||||||
|
projectName: reminder.projectName || "",
|
||||||
|
organizationName: reminder.organizationName || ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (reminder.repeatDays) {
|
||||||
|
await reminderDAL.updateById(reminder.id, { nextReminderDate: $addDays(reminder.repeatDays) }, tx);
|
||||||
|
} else {
|
||||||
|
await reminderDAL.deleteById(reminder.id, tx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
`Failed to send reminder to recipients ${reminder.recipients.map((r) => r.email).join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteReminder: TReminderServiceFactory["deleteReminder"] = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
secretId
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
secretId: string;
|
||||||
|
}) => {
|
||||||
|
const secret = await secretV2BridgeDAL.findOneWithTags({ [`${TableName.SecretV2}.id` as "id"]: secretId });
|
||||||
|
if (!secret) {
|
||||||
|
throw new BadRequestError({ message: `Secret ${secretId} not found` });
|
||||||
|
}
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: secret.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionSecretActions.Edit, ProjectPermissionSub.Secrets);
|
||||||
|
await reminderDAL.delete({ secretId });
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secret.projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteReminderBySecretId: TReminderServiceFactory["deleteReminderBySecretId"] = async (
|
||||||
|
secretId: string,
|
||||||
|
projectId: string,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
await reminderDAL.delete({ secretId }, tx);
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const batchCreateReminders: TReminderServiceFactory["batchCreateReminders"] = async (
|
||||||
|
remindersData: TBatchCreateReminderDTO,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
if (!remindersData || remindersData.length === 0) {
|
||||||
|
return { created: 0, reminderIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedReminders = remindersData.map(
|
||||||
|
({ secretId, message, repeatDays, nextReminderDate: nextReminderDateInput, recipients, projectId }) => {
|
||||||
|
let nextReminderDate;
|
||||||
|
if (nextReminderDateInput) {
|
||||||
|
nextReminderDate = new Date(nextReminderDateInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repeatDays && repeatDays > 0 && !nextReminderDate) {
|
||||||
|
nextReminderDate = $addDays(repeatDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextReminderDate) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `repeatDays must be a positive number for secretId: ${secretId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretId,
|
||||||
|
message,
|
||||||
|
repeatDays,
|
||||||
|
nextReminderDate,
|
||||||
|
recipients: recipients ? [...new Set(recipients)] : [],
|
||||||
|
projectId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const newReminders = await reminderDAL.insertMany(
|
||||||
|
processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, projectId }) => ({
|
||||||
|
secretId,
|
||||||
|
message,
|
||||||
|
repeatDays,
|
||||||
|
nextReminderDate,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const allRecipientInserts: Array<{ reminderId: string; userId: string }> = [];
|
||||||
|
|
||||||
|
newReminders.forEach((reminder, index) => {
|
||||||
|
const { recipients } = processedReminders[index];
|
||||||
|
if (recipients && recipients.length > 0) {
|
||||||
|
recipients.forEach((userId) => {
|
||||||
|
allRecipientInserts.push({
|
||||||
|
reminderId: reminder.id,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allRecipientInserts.length > 0) {
|
||||||
|
await reminderRecipientDAL.insertMany(allRecipientInserts, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectIds = new Set(processedReminders.map((r) => r.projectId).filter((id): id is string => Boolean(id)));
|
||||||
|
for (const projectId of projectIds) {
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
created: newReminders.length,
|
||||||
|
reminderIds: newReminders.map((r) => r.id)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createReminder,
|
||||||
|
getReminder,
|
||||||
|
sendDailyReminders,
|
||||||
|
deleteReminder,
|
||||||
|
deleteReminderBySecretId,
|
||||||
|
batchCreateReminders,
|
||||||
|
createReminderInternal
|
||||||
|
};
|
||||||
|
};
|
102
backend/src/services/reminder/reminder-types.ts
Normal file
102
backend/src/services/reminder/reminder-types.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
|
export type TReminder = {
|
||||||
|
id: string;
|
||||||
|
secretId?: string | null;
|
||||||
|
message?: string | null;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
nextReminderDate: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateReminderDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
reminder: {
|
||||||
|
secretId?: string;
|
||||||
|
message?: string | null;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
nextReminderDate?: string | null;
|
||||||
|
recipients?: string[] | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBatchCreateReminderDTO = {
|
||||||
|
secretId: string;
|
||||||
|
message?: string | null;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
nextReminderDate?: string | Date | null;
|
||||||
|
recipients?: string[] | null;
|
||||||
|
projectId?: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export interface TReminderServiceFactory {
|
||||||
|
createReminder: ({ actor, actorId, actorOrgId, actorAuthMethod, reminder }: TCreateReminderDTO) => Promise<{
|
||||||
|
id: string;
|
||||||
|
created: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
getReminder: ({
|
||||||
|
secretId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: {
|
||||||
|
secretId: string;
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
}) => Promise<(TReminder & { recipients: string[] }) | null>;
|
||||||
|
|
||||||
|
sendDailyReminders: () => Promise<void>;
|
||||||
|
|
||||||
|
deleteReminder: ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
secretId
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
secretId: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
deleteReminderBySecretId: (secretId: string, projectId: string, tx?: Knex) => Promise<void>;
|
||||||
|
|
||||||
|
batchCreateReminders: (
|
||||||
|
remindersData: TBatchCreateReminderDTO,
|
||||||
|
tx?: Knex
|
||||||
|
) => Promise<{
|
||||||
|
created: number;
|
||||||
|
reminderIds: string[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
createReminderInternal: ({
|
||||||
|
secretId,
|
||||||
|
message,
|
||||||
|
repeatDays,
|
||||||
|
nextReminderDate,
|
||||||
|
recipients,
|
||||||
|
projectId
|
||||||
|
}: {
|
||||||
|
secretId?: string;
|
||||||
|
message?: string | null;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
nextReminderDate?: string | null;
|
||||||
|
recipients?: string[] | null;
|
||||||
|
projectId: string;
|
||||||
|
}) => Promise<{
|
||||||
|
id: string;
|
||||||
|
created: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
@ -6,7 +6,6 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
|||||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||||
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
|
||||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||||
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||||
@ -19,7 +18,6 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
|||||||
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
||||||
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
||||||
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
|
|
||||||
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">;
|
||||||
@ -36,7 +34,6 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretFolderVersionDAL,
|
secretFolderVersionDAL,
|
||||||
secretDAL,
|
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL,
|
secretVersionV2DAL,
|
||||||
@ -46,7 +43,6 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||||
await secretDAL.pruneSecretReminders(queueService);
|
|
||||||
await identityAccessTokenDAL.removeExpiredTokens();
|
await identityAccessTokenDAL.removeExpiredTokens();
|
||||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||||
await secretSharingDAL.pruneExpiredSharedSecrets();
|
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||||
|
@ -466,6 +466,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This method currently uses too many joins which is not performant, in case we need to add more filters we should consider refactoring this method
|
||||||
const findByFolderIds = async (dto: {
|
const findByFolderIds = async (dto: {
|
||||||
folderIds: string[];
|
folderIds: string[];
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@ -513,18 +514,15 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||||
`${TableName.SecretTag}.id`
|
`${TableName.SecretTag}.id`
|
||||||
)
|
)
|
||||||
.leftJoin(
|
|
||||||
TableName.SecretReminderRecipients,
|
|
||||||
`${TableName.SecretV2}.id`,
|
|
||||||
`${TableName.SecretReminderRecipients}.secretId`
|
|
||||||
)
|
|
||||||
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
|
|
||||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretRotationV2SecretMapping,
|
TableName.SecretRotationV2SecretMapping,
|
||||||
`${TableName.SecretV2}.id`,
|
`${TableName.SecretV2}.id`,
|
||||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||||
)
|
)
|
||||||
|
.leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`)
|
||||||
|
.leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.ReminderRecipient}.userId`, `${TableName.Users}.id`)
|
||||||
.where((qb) => {
|
.where((qb) => {
|
||||||
if (filters?.metadataFilter && filters.metadataFilter.length > 0) {
|
if (filters?.metadataFilter && filters.metadataFilter.length > 0) {
|
||||||
filters.metadataFilter.forEach((meta) => {
|
filters.metadataFilter.forEach((meta) => {
|
||||||
@ -547,7 +545,11 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
}) as rank`
|
}) as rank`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.select(db.ref("id").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientId"))
|
.select(db.ref("id").withSchema(TableName.Reminder).as("reminderId"))
|
||||||
|
.select(db.ref("message").withSchema(TableName.Reminder).as("reminderNote"))
|
||||||
|
.select(db.ref("repeatDays").withSchema(TableName.Reminder).as("reminderRepeatDays"))
|
||||||
|
.select(db.ref("nextReminderDate").withSchema(TableName.Reminder).as("nextReminderDate"))
|
||||||
|
.select(db.ref("id").withSchema(TableName.ReminderRecipient).as("reminderRecipientId"))
|
||||||
.select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername"))
|
.select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername"))
|
||||||
.select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail"))
|
.select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail"))
|
||||||
.select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId"))
|
.select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId"))
|
||||||
@ -809,6 +811,44 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findSecretsWithReminderRecipients = async (ids: string[], limit: number, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
// Create a subquery to get limited secret IDs
|
||||||
|
const limitedSecretIds = (tx || db)(TableName.SecretV2)
|
||||||
|
.whereIn(`${TableName.SecretV2}.id`, ids)
|
||||||
|
.limit(limit)
|
||||||
|
.select("id");
|
||||||
|
|
||||||
|
// Join with all recipients for the limited secrets
|
||||||
|
const docs = await (tx || db)(TableName.SecretV2)
|
||||||
|
.whereIn(`${TableName.SecretV2}.id`, limitedSecretIds)
|
||||||
|
.leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`)
|
||||||
|
.leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`)
|
||||||
|
.select(selectAllTableCols(TableName.SecretV2))
|
||||||
|
.select(db.ref("userId").withSchema(TableName.ReminderRecipient).as("reminderRecipientUserId"));
|
||||||
|
|
||||||
|
const data = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({
|
||||||
|
_id: el.id,
|
||||||
|
...SecretsV2Schema.parse(el)
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "reminderRecipientUserId",
|
||||||
|
label: "recipients" as const,
|
||||||
|
mapper: ({ reminderRecipientUserId }) => reminderRecipientUserId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindSecretsWithReminderRecipients" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretOrm,
|
...secretOrm,
|
||||||
update,
|
update,
|
||||||
@ -826,6 +866,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
countByFolderIds,
|
countByFolderIds,
|
||||||
findOne,
|
findOne,
|
||||||
find,
|
find,
|
||||||
invalidateSecretCacheByProjectId
|
invalidateSecretCacheByProjectId,
|
||||||
|
findSecretsWithReminderRecipients
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -223,20 +223,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
const actorType = actor?.type || ActorType.PLATFORM;
|
const actorType = actor?.type || ActorType.PLATFORM;
|
||||||
|
|
||||||
const sanitizedInputSecrets = inputSecrets.map(
|
const sanitizedInputSecrets = inputSecrets.map(
|
||||||
({
|
({ filter, data: { skipMultilineEncoding, type, key, encryptedValue, userId, encryptedComment, metadata } }) => ({
|
||||||
filter,
|
|
||||||
data: {
|
|
||||||
skipMultilineEncoding,
|
|
||||||
type,
|
|
||||||
key,
|
|
||||||
encryptedValue,
|
|
||||||
userId,
|
|
||||||
encryptedComment,
|
|
||||||
metadata,
|
|
||||||
reminderNote,
|
|
||||||
reminderRepeatDays
|
|
||||||
}
|
|
||||||
}) => ({
|
|
||||||
filter: { ...filter, folderId },
|
filter: { ...filter, folderId },
|
||||||
data: {
|
data: {
|
||||||
skipMultilineEncoding,
|
skipMultilineEncoding,
|
||||||
@ -245,9 +232,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
userId,
|
userId,
|
||||||
encryptedComment,
|
encryptedComment,
|
||||||
metadata,
|
metadata,
|
||||||
reminderNote,
|
encryptedValue
|
||||||
encryptedValue,
|
|
||||||
reminderRepeatDays
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -263,9 +248,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
encryptedComment,
|
encryptedComment,
|
||||||
version,
|
version,
|
||||||
metadata,
|
metadata,
|
||||||
reminderNote,
|
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
reminderRepeatDays,
|
|
||||||
id: secretId
|
id: secretId
|
||||||
}) => ({
|
}) => ({
|
||||||
skipMultilineEncoding,
|
skipMultilineEncoding,
|
||||||
@ -275,9 +258,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
encryptedComment,
|
encryptedComment,
|
||||||
version,
|
version,
|
||||||
metadata: metadata ? JSON.stringify(metadata) : [],
|
metadata: metadata ? JSON.stringify(metadata) : [],
|
||||||
reminderNote,
|
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
reminderRepeatDays,
|
|
||||||
folderId,
|
folderId,
|
||||||
secretId,
|
secretId,
|
||||||
userActorId,
|
userActorId,
|
||||||
@ -395,7 +376,8 @@ export const fnSecretBulkDelete = async ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
folderCommitService,
|
folderCommitService,
|
||||||
secretVersionDAL
|
secretVersionDAL,
|
||||||
|
projectId
|
||||||
}: TFnSecretBulkDelete) => {
|
}: TFnSecretBulkDelete) => {
|
||||||
const deletedSecrets = await secretDAL.deleteMany(
|
const deletedSecrets = await secretDAL.deleteMany(
|
||||||
inputSecrets.map(({ type, secretKey }) => ({
|
inputSecrets.map(({ type, secretKey }) => ({
|
||||||
@ -407,11 +389,14 @@ export const fnSecretBulkDelete = async ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.all(
|
||||||
deletedSecrets
|
deletedSecrets
|
||||||
.filter(({ reminderRepeatDays }) => Boolean(reminderRepeatDays))
|
.filter(({ reminderRepeatDays }) => Boolean(reminderRepeatDays))
|
||||||
.map(({ id, reminderRepeatDays }) =>
|
.map(({ id, reminderRepeatDays }) =>
|
||||||
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: reminderRepeatDays as number }, tx)
|
secretQueueService.removeSecretReminder(
|
||||||
|
{ secretId: id, repeatDays: reminderRepeatDays as number, projectId },
|
||||||
|
tx
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-serv
|
|||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TReminderServiceFactory } from "../reminder/reminder-types";
|
||||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||||
import { TGetASecretByIdDTO } from "../secret/secret-types";
|
import { TGetASecretByIdDTO } from "../secret/secret-types";
|
||||||
@ -108,6 +109,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
|||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setExpiry" | "setItemWithExpiry" | "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "getItem" | "setExpiry" | "setItemWithExpiry" | "deleteItem">;
|
||||||
|
reminderService: Pick<TReminderServiceFactory, "createReminder" | "getReminder">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
|
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
|
||||||
@ -132,7 +134,8 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
keyStore
|
keyStore,
|
||||||
|
reminderService
|
||||||
}: TSecretV2BridgeServiceFactoryDep) => {
|
}: TSecretV2BridgeServiceFactoryDep) => {
|
||||||
const $validateSecretReferences = async (
|
const $validateSecretReferences = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -303,7 +306,6 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
{
|
{
|
||||||
version: 1,
|
version: 1,
|
||||||
type,
|
type,
|
||||||
reminderRepeatDays: inputSecretData.secretReminderRepeatDays,
|
|
||||||
encryptedComment: setKnexStringValue(
|
encryptedComment: setKnexStringValue(
|
||||||
inputSecretData.secretComment,
|
inputSecretData.secretComment,
|
||||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||||
@ -311,7 +313,6 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
encryptedValue: inputSecretData.secretValue
|
encryptedValue: inputSecretData.secretValue
|
||||||
? secretManagerEncryptor({ plainText: Buffer.from(inputSecretData.secretValue) }).cipherTextBlob
|
? secretManagerEncryptor({ plainText: Buffer.from(inputSecretData.secretValue) }).cipherTextBlob
|
||||||
: undefined,
|
: undefined,
|
||||||
reminderNote: inputSecretData.secretReminderNote,
|
|
||||||
skipMultilineEncoding: inputSecretData.skipMultilineEncoding,
|
skipMultilineEncoding: inputSecretData.skipMultilineEncoding,
|
||||||
key: secretName,
|
key: secretName,
|
||||||
userId: inputSecret.type === SecretType.Personal ? actorId : null,
|
userId: inputSecret.type === SecretType.Personal ? actorId : null,
|
||||||
@ -337,6 +338,20 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
return createdSecret;
|
return createdSecret;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (inputSecret.secretReminderRepeatDays) {
|
||||||
|
await reminderService.createReminder({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
reminder: {
|
||||||
|
secretId: secret.id,
|
||||||
|
message: inputSecret.secretReminderNote,
|
||||||
|
repeatDays: inputSecret.secretReminderRepeatDays
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await secretDAL.invalidateSecretCacheByProjectId(projectId);
|
await secretDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
if (inputSecret.type === SecretType.Shared) {
|
if (inputSecret.type === SecretType.Shared) {
|
||||||
await snapshotService.performSnapshot(folderId);
|
await snapshotService.performSnapshot(folderId);
|
||||||
@ -512,12 +527,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
{
|
{
|
||||||
filter: { id: secretId },
|
filter: { id: secretId },
|
||||||
data: {
|
data: {
|
||||||
reminderRepeatDays: inputSecret.secretReminderRepeatDays,
|
|
||||||
encryptedComment: setKnexStringValue(
|
encryptedComment: setKnexStringValue(
|
||||||
inputSecret.secretComment,
|
inputSecret.secretComment,
|
||||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||||
),
|
),
|
||||||
reminderNote: inputSecret.secretReminderNote,
|
|
||||||
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
|
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
|
||||||
key: inputSecret.newSecretName || secretName,
|
key: inputSecret.newSecretName || secretName,
|
||||||
tags: inputSecret.tagIds,
|
tags: inputSecret.tagIds,
|
||||||
@ -538,19 +551,20 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await secretQueueService.handleSecretReminder({
|
if (inputSecret.secretReminderRepeatDays) {
|
||||||
newSecret: {
|
await reminderService.createReminder({
|
||||||
id: updatedSecret[0].id,
|
actor,
|
||||||
...inputSecret
|
actorId,
|
||||||
},
|
actorOrgId,
|
||||||
oldSecret: {
|
actorAuthMethod,
|
||||||
id: secret.id,
|
reminder: {
|
||||||
secretReminderNote: secret.reminderNote,
|
secretId: secret.id,
|
||||||
secretReminderRepeatDays: secret.reminderRepeatDays,
|
message: inputSecret.secretReminderNote,
|
||||||
secretReminderRecipients: secret.secretReminderRecipients?.map((el) => el.user.id)
|
repeatDays: inputSecret.secretReminderRepeatDays,
|
||||||
},
|
recipients: inputSecret.secretReminderRecipients
|
||||||
projectId
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await secretDAL.invalidateSecretCacheByProjectId(projectId);
|
await secretDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
if (inputSecret.type === SecretType.Shared) {
|
if (inputSecret.type === SecretType.Shared) {
|
||||||
@ -1868,12 +1882,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
filter: { id: originalSecret.id, type: SecretType.Shared },
|
filter: { id: originalSecret.id, type: SecretType.Shared },
|
||||||
data: {
|
data: {
|
||||||
reminderRepeatDays: el.secretReminderRepeatDays,
|
|
||||||
encryptedComment: setKnexStringValue(
|
encryptedComment: setKnexStringValue(
|
||||||
el.secretComment,
|
el.secretComment,
|
||||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||||
),
|
),
|
||||||
reminderNote: el.secretReminderNote,
|
|
||||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
key: el.newSecretName || el.secretKey,
|
key: el.newSecretName || el.secretKey,
|
||||||
tags: el.tagIds,
|
tags: el.tagIds,
|
||||||
@ -2522,9 +2534,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
key: doc.key,
|
key: doc.key,
|
||||||
encryptedComment: doc.encryptedComment,
|
encryptedComment: doc.encryptedComment,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||||
reminderNote: doc.reminderNote,
|
|
||||||
secretMetadata: doc.secretMetadata,
|
secretMetadata: doc.secretMetadata,
|
||||||
reminderRepeatDays: doc.reminderRepeatDays,
|
|
||||||
...(doc.encryptedValue
|
...(doc.encryptedValue
|
||||||
? {
|
? {
|
||||||
encryptedValue: doc.encryptedValue,
|
encryptedValue: doc.encryptedValue,
|
||||||
@ -2914,6 +2924,11 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findSecretIdsByFolderIdAndKeys = async ({ folderId, keys }: { folderId: string; keys: string[] }) => {
|
||||||
|
const secrets = await secretDAL.find({ folderId, $in: { [`${TableName.SecretV2}.key` as "key"]: keys } });
|
||||||
|
return secrets.map((el) => ({ id: el.id, key: el.key }));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createSecret,
|
createSecret,
|
||||||
deleteSecret,
|
deleteSecret,
|
||||||
@ -2933,6 +2948,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
getSecretsByFolderMappings,
|
getSecretsByFolderMappings,
|
||||||
getSecretById,
|
getSecretById,
|
||||||
getAccessibleSecrets,
|
getAccessibleSecrets,
|
||||||
getSecretVersionsByIds
|
getSecretVersionsByIds,
|
||||||
|
findSecretIdsByFolderIdAndKeys
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -246,6 +246,7 @@ export type TCreateSecretReminderDTO = {
|
|||||||
export type TRemoveSecretReminderDTO = {
|
export type TRemoveSecretReminderDTO = {
|
||||||
secretId: string;
|
secretId: string;
|
||||||
repeatDays: number;
|
repeatDays: number;
|
||||||
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TBackFillSecretReferencesDTO = TProjectPermission;
|
export type TBackFillSecretReferencesDTO = TProjectPermission;
|
||||||
|
@ -5,8 +5,6 @@ import { TDbClient } from "@app/db";
|
|||||||
import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
|
import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
|
||||||
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
|
||||||
|
|
||||||
export type TSecretDALFactory = ReturnType<typeof secretDALFactory>;
|
export type TSecretDALFactory = ReturnType<typeof secretDALFactory>;
|
||||||
|
|
||||||
@ -383,94 +381,6 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pruneSecretReminders = async (queueService: TQueueServiceFactory) => {
|
|
||||||
const REMINDER_PRUNE_BATCH_SIZE = 5_000;
|
|
||||||
const MAX_RETRY_ON_FAILURE = 3;
|
|
||||||
let numberOfRetryOnFailure = 0;
|
|
||||||
let deletedReminderCount = 0;
|
|
||||||
|
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: secret reminders started`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const repeatableJobs = await queueService.getRepeatableJobs(QueueName.SecretReminder);
|
|
||||||
const reminderJobs = repeatableJobs
|
|
||||||
.map((job) => ({ secretId: job.id?.replace("reminder-", "") as string, jobKey: job.key }))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (reminderJobs.length === 0) {
|
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: no reminder jobs found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let offset = 0; offset < reminderJobs.length; offset += REMINDER_PRUNE_BATCH_SIZE) {
|
|
||||||
try {
|
|
||||||
const batchIds = reminderJobs.slice(offset, offset + REMINDER_PRUNE_BATCH_SIZE).map((r) => r.secretId);
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
$in: {
|
|
||||||
id: batchIds
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
limit: REMINDER_PRUNE_BATCH_SIZE
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find existing secrets with pagination
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const [secrets, secretsV2] = await Promise.all([
|
|
||||||
ormify(db, TableName.Secret).find(payload, opts),
|
|
||||||
ormify(db, TableName.SecretV2).find(payload, opts)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const foundSecretIds = new Set([
|
|
||||||
...secrets.map((secret) => secret.id),
|
|
||||||
...secretsV2.map((secret) => secret.id)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Find IDs that don't exist in either table
|
|
||||||
const secretIdsNotFound = batchIds.filter((secretId) => !foundSecretIds.has(secretId));
|
|
||||||
|
|
||||||
// Delete reminders for non-existent secrets
|
|
||||||
for (const secretId of secretIdsNotFound) {
|
|
||||||
const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey;
|
|
||||||
|
|
||||||
if (jobKey) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey);
|
|
||||||
deletedReminderCount += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
numberOfRetryOnFailure = 0;
|
|
||||||
} catch (error) {
|
|
||||||
numberOfRetryOnFailure += 1;
|
|
||||||
logger.error(error, `Failed to process batch at offset ${offset}`);
|
|
||||||
|
|
||||||
if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry the current batch
|
|
||||||
offset -= REMINDER_PRUNE_BATCH_SIZE;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500 * numberOfRetryOnFailure));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay between batches
|
|
||||||
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error, "Failed to complete secret reminder pruning");
|
|
||||||
} finally {
|
|
||||||
logger.info(
|
|
||||||
`${QueueName.DailyResourceCleanUp}: secret reminders completed. Deleted ${deletedReminderCount} reminders`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretOrm,
|
...secretOrm,
|
||||||
update,
|
update,
|
||||||
@ -485,7 +395,6 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
upsertSecretReferences,
|
upsertSecretReferences,
|
||||||
findReferencedSecretReferences,
|
findReferencedSecretReferences,
|
||||||
findAllProjectSecretValues,
|
findAllProjectSecretValues,
|
||||||
pruneSecretReminders,
|
|
||||||
findManySecretsWithTags
|
findManySecretsWithTags
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -20,11 +20,9 @@ import {
|
|||||||
decryptSymmetric128BitHexKeyUTF8,
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
encryptSymmetric128BitHexKeyUTF8
|
encryptSymmetric128BitHexKeyUTF8
|
||||||
} from "@app/lib/crypto";
|
} from "@app/lib/crypto";
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy, unique } from "@app/lib/fn";
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
|
||||||
import {
|
import {
|
||||||
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
||||||
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
||||||
@ -36,6 +34,7 @@ import { KmsDataKey } from "../kms/kms-types";
|
|||||||
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TReminderServiceFactory } from "../reminder/reminder-types";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TSecretDALFactory } from "./secret-dal";
|
import { TSecretDALFactory } from "./secret-dal";
|
||||||
@ -741,7 +740,8 @@ export const fnSecretBulkDelete = async ({
|
|||||||
tx,
|
tx,
|
||||||
actorId,
|
actorId,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretQueueService
|
secretQueueService,
|
||||||
|
projectId
|
||||||
}: TFnSecretBulkDelete) => {
|
}: TFnSecretBulkDelete) => {
|
||||||
const deletedSecrets = await secretDAL.deleteMany(
|
const deletedSecrets = await secretDAL.deleteMany(
|
||||||
inputSecrets.map(({ type, secretBlindIndex }) => ({
|
inputSecrets.map(({ type, secretBlindIndex }) => ({
|
||||||
@ -757,7 +757,10 @@ export const fnSecretBulkDelete = async ({
|
|||||||
deletedSecrets
|
deletedSecrets
|
||||||
.filter(({ secretReminderRepeatDays }) => Boolean(secretReminderRepeatDays))
|
.filter(({ secretReminderRepeatDays }) => Boolean(secretReminderRepeatDays))
|
||||||
.map(({ id, secretReminderRepeatDays }) =>
|
.map(({ id, secretReminderRepeatDays }) =>
|
||||||
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: secretReminderRepeatDays as number }, tx)
|
secretQueueService.removeSecretReminder(
|
||||||
|
{ secretId: id, repeatDays: secretReminderRepeatDays as number, projectId },
|
||||||
|
tx
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1184,14 +1187,14 @@ export const decryptSecretWithBot = (
|
|||||||
type TFnDeleteProjectSecretReminders = {
|
type TFnDeleteProjectSecretReminders = {
|
||||||
secretDAL: Pick<TSecretDALFactory, "find">;
|
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fnDeleteProjectSecretReminders = async (
|
export const fnDeleteProjectSecretReminders = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
{ secretDAL, secretV2BridgeDAL, queueService, projectBotService, folderDAL }: TFnDeleteProjectSecretReminders
|
{ secretDAL, secretV2BridgeDAL, reminderService, projectBotService, folderDAL }: TFnDeleteProjectSecretReminders
|
||||||
) => {
|
) => {
|
||||||
const projectFolders = await folderDAL.findByProjectId(projectId);
|
const projectFolders = await folderDAL.findByProjectId(projectId);
|
||||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId, false);
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId, false);
|
||||||
@ -1206,23 +1209,13 @@ export const fnDeleteProjectSecretReminders = async (
|
|||||||
$notNull: ["secretReminderRepeatDays"]
|
$notNull: ["secretReminderRepeatDays"]
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
for await (const secret of projectSecrets) {
|
for await (const secret of projectSecrets) {
|
||||||
const repeatDays = shouldUseSecretV2Bridge
|
const repeatDays = shouldUseSecretV2Bridge
|
||||||
? (secret as { reminderRepeatDays: number }).reminderRepeatDays
|
? (secret as { reminderRepeatDays: number }).reminderRepeatDays
|
||||||
: (secret as { secretReminderRepeatDays: number }).secretReminderRepeatDays;
|
: (secret as { secretReminderRepeatDays: number }).secretReminderRepeatDays;
|
||||||
|
|
||||||
// We're using the queue service directly to get around conflicting imports.
|
|
||||||
if (repeatDays) {
|
if (repeatDays) {
|
||||||
await queueService.stopRepeatableJob(
|
await reminderService.deleteReminderBySecretId(secret.id, projectId);
|
||||||
QueueName.SecretReminder,
|
|
||||||
QueueJobs.SecretReminder,
|
|
||||||
{
|
|
||||||
// on prod it this will be in days, in development this will be second
|
|
||||||
every: appCfg.NODE_ENV === "development" ? secondsToMillis(repeatDays) : daysToMillisecond(repeatDays)
|
|
||||||
},
|
|
||||||
`reminder-${secret.id}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,6 @@ import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn";
|
import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@ -41,7 +40,6 @@ import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-
|
|||||||
import { syncIntegrationSecrets } from "../integration-auth/integration-sync-secret";
|
import { syncIntegrationSecrets } from "../integration-auth/integration-sync-secret";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { createProjectKey } from "../project/project-fns";
|
import { createProjectKey } from "../project/project-fns";
|
||||||
@ -50,12 +48,12 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
|||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
|
import { TReminderServiceFactory } from "../reminder/reminder-types";
|
||||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||||
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
|
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||||
@ -93,7 +91,6 @@ type TSecretQueueFactoryDep = {
|
|||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "create">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers" | "create">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers" | "create">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
|
|
||||||
secretVersionDAL: TSecretVersionDALFactory;
|
secretVersionDAL: TSecretVersionDALFactory;
|
||||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||||
secretTagDAL: TSecretTagDALFactory;
|
secretTagDAL: TSecretTagDALFactory;
|
||||||
@ -113,11 +110,8 @@ type TSecretQueueFactoryDep = {
|
|||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
secretReminderRecipientsDAL: Pick<
|
|
||||||
TSecretReminderRecipientsDALFactory,
|
|
||||||
"delete" | "findUsersBySecretId" | "insertMany" | "transaction"
|
|
||||||
>;
|
|
||||||
secretSyncQueue: Pick<TSecretSyncQueueFactory, "queueSecretSyncsSyncSecretsByPath">;
|
secretSyncQueue: Pick<TSecretSyncQueueFactory, "queueSecretSyncsSyncSecretsByPath">;
|
||||||
|
reminderService: Pick<TReminderServiceFactory, "createReminderInternal" | "deleteReminderBySecretId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecrets = {
|
export type TGetSecrets = {
|
||||||
@ -155,7 +149,6 @@ export const secretQueueFactory = ({
|
|||||||
userDAL,
|
userDAL,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
orgDAL,
|
|
||||||
smtpService,
|
smtpService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@ -178,9 +171,9 @@ export const secretQueueFactory = ({
|
|||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretReminderRecipientsDAL,
|
|
||||||
secretSyncQueue,
|
secretSyncQueue,
|
||||||
folderCommitService
|
folderCommitService,
|
||||||
|
reminderService
|
||||||
}: TSecretQueueFactoryDep) => {
|
}: TSecretQueueFactoryDep) => {
|
||||||
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
||||||
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
||||||
@ -190,19 +183,8 @@ export const secretQueueFactory = ({
|
|||||||
|
|
||||||
const removeSecretReminder = async ({ deleteRecipients = true, ...dto }: TRemoveSecretReminderDTO, tx?: Knex) => {
|
const removeSecretReminder = async ({ deleteRecipients = true, ...dto }: TRemoveSecretReminderDTO, tx?: Knex) => {
|
||||||
if (deleteRecipients) {
|
if (deleteRecipients) {
|
||||||
await secretReminderRecipientsDAL.delete({ secretId: dto.secretId }, tx);
|
await reminderService.deleteReminderBySecretId(dto.secretId, dto.projectId, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
await queueService.stopRepeatableJob(
|
|
||||||
QueueName.SecretReminder,
|
|
||||||
QueueJobs.SecretReminder,
|
|
||||||
{
|
|
||||||
// on prod it this will be in days, in development this will be second
|
|
||||||
every: appCfg.NODE_ENV === "development" ? secondsToMillis(dto.repeatDays) : daysToMillisecond(dto.repeatDays)
|
|
||||||
},
|
|
||||||
`reminder-${dto.secretId}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const $generateActor = async (actorId?: string, isManual?: boolean): Promise<Actor> => {
|
const $generateActor = async (actorId?: string, isManual?: boolean): Promise<Actor> => {
|
||||||
@ -242,11 +224,9 @@ export const secretQueueFactory = ({
|
|||||||
oldSecret,
|
oldSecret,
|
||||||
newSecret,
|
newSecret,
|
||||||
projectId,
|
projectId,
|
||||||
deleteRecipients = true
|
secretReminderRecipients
|
||||||
}: TCreateSecretReminderDTO) => {
|
}: TCreateSecretReminderDTO) => {
|
||||||
try {
|
try {
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
if (oldSecret.id !== newSecret.id) {
|
if (oldSecret.id !== newSecret.id) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
name: "SecretReminderIdMismatch",
|
name: "SecretReminderIdMismatch",
|
||||||
@ -261,38 +241,13 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the secret already has a reminder, we should remove the existing one first.
|
await reminderService.createReminderInternal({
|
||||||
if (oldSecret.secretReminderRepeatDays) {
|
secretId: newSecret.id,
|
||||||
await removeSecretReminder({
|
message: newSecret.secretReminderNote,
|
||||||
repeatDays: oldSecret.secretReminderRepeatDays,
|
|
||||||
secretId: oldSecret.id,
|
|
||||||
deleteRecipients
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await queueService.queue(
|
|
||||||
QueueName.SecretReminder,
|
|
||||||
QueueJobs.SecretReminder,
|
|
||||||
{
|
|
||||||
note: newSecret.secretReminderNote,
|
|
||||||
projectId,
|
|
||||||
repeatDays: newSecret.secretReminderRepeatDays,
|
repeatDays: newSecret.secretReminderRepeatDays,
|
||||||
secretId: newSecret.id
|
recipients: secretReminderRecipients,
|
||||||
},
|
projectId
|
||||||
{
|
});
|
||||||
jobId: `reminder-${newSecret.id}`,
|
|
||||||
repeat: {
|
|
||||||
// on prod it this will be in days, in development this will be second
|
|
||||||
every:
|
|
||||||
appCfg.NODE_ENV === "development"
|
|
||||||
? secondsToMillis(newSecret.secretReminderRepeatDays)
|
|
||||||
: daysToMillisecond(newSecret.secretReminderRepeatDays),
|
|
||||||
immediately: true
|
|
||||||
},
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err, "Failed to create secret reminder.");
|
logger.error(err, "Failed to create secret reminder.");
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@ -305,12 +260,6 @@ export const secretQueueFactory = ({
|
|||||||
const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => {
|
const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => {
|
||||||
const { secretReminderRepeatDays, secretReminderNote, secretReminderRecipients } = newSecret;
|
const { secretReminderRepeatDays, secretReminderNote, secretReminderRecipients } = newSecret;
|
||||||
|
|
||||||
const recipientsUpdated =
|
|
||||||
secretReminderRecipients?.some(
|
|
||||||
(newId) => !oldSecret.secretReminderRecipients?.find((oldId) => newId === oldId)
|
|
||||||
) || secretReminderRecipients?.length !== oldSecret.secretReminderRecipients?.length;
|
|
||||||
|
|
||||||
await secretReminderRecipientsDAL.transaction(async (tx) => {
|
|
||||||
if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) {
|
if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) {
|
||||||
if (
|
if (
|
||||||
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
|
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
|
||||||
@ -320,6 +269,7 @@ export const secretQueueFactory = ({
|
|||||||
oldSecret,
|
oldSecret,
|
||||||
newSecret,
|
newSecret,
|
||||||
projectId,
|
projectId,
|
||||||
|
secretReminderRecipients: secretReminderRecipients ?? [],
|
||||||
deleteRecipients: false
|
deleteRecipients: false
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
@ -329,31 +279,11 @@ export const secretQueueFactory = ({
|
|||||||
) {
|
) {
|
||||||
await removeSecretReminder({
|
await removeSecretReminder({
|
||||||
secretId: oldSecret.id,
|
secretId: oldSecret.id,
|
||||||
repeatDays: oldSecret.secretReminderRepeatDays
|
repeatDays: oldSecret.secretReminderRepeatDays,
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recipientsUpdated) {
|
|
||||||
// if no recipients, delete all existing recipients
|
|
||||||
if (!secretReminderRecipients?.length) {
|
|
||||||
const existingRecipients = await secretReminderRecipientsDAL.findUsersBySecretId(newSecret.id, tx);
|
|
||||||
if (existingRecipients) {
|
|
||||||
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
|
|
||||||
await secretReminderRecipientsDAL.insertMany(
|
|
||||||
secretReminderRecipients.map((r) => ({
|
|
||||||
secretId: newSecret.id,
|
|
||||||
userId: r,
|
|
||||||
projectId
|
projectId
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const createManySecretsRawFn = createManySecretsRawFnFactory({
|
const createManySecretsRawFn = createManySecretsRawFnFactory({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@ -1111,62 +1041,9 @@ export const secretQueueFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO(Carlos): remove this queue (needed for queue initialization and perform the migration)
|
||||||
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
||||||
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}]`);
|
logger.info(`(deprecated) secretReminderQueue.process: [secretDocument=${data.secretId}]`);
|
||||||
|
|
||||||
const { projectId } = data;
|
|
||||||
|
|
||||||
const organization = await orgDAL.findOrgByProjectId(projectId);
|
|
||||||
const project = await projectDAL.findById(projectId);
|
|
||||||
const secret = await secretV2BridgeDAL.findById(data.secretId);
|
|
||||||
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
|
|
||||||
|
|
||||||
const recipients = await secretReminderRecipientsDAL.findUsersBySecretId(data.secretId);
|
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no project found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
|
||||||
|
|
||||||
if (!projectMembers || !projectMembers.length) {
|
|
||||||
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no project members found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedRecipients = recipients?.length
|
|
||||||
? recipients.map((r) => r.email as string)
|
|
||||||
: projectMembers.map((m) => m.user.email as string);
|
|
||||||
|
|
||||||
await smtpService.sendMail({
|
|
||||||
template: SmtpTemplates.SecretReminder,
|
|
||||||
subjectLine: "Infisical secret reminder",
|
|
||||||
recipients: selectedRecipients,
|
|
||||||
substitutions: {
|
|
||||||
reminderNote: data.note, // May not be present.
|
|
||||||
projectName: project.name,
|
|
||||||
organizationName: organization.name
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
|
|
||||||
type: WebhookEvents.SecretReminderExpired,
|
|
||||||
payload: {
|
|
||||||
projectName: project.name,
|
|
||||||
projectId: project.id,
|
|
||||||
secretPath: folder?.path,
|
|
||||||
environment: folder?.environmentSlug || "",
|
|
||||||
reminderNote: data.note,
|
|
||||||
secretName: secret?.key,
|
|
||||||
secretId: data.secretId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const startSecretV2Migration = async (projectId: string) => {
|
const startSecretV2Migration = async (projectId: string) => {
|
||||||
|
@ -48,6 +48,7 @@ import { ChangeType } from "../folder-commit/folder-commit-service";
|
|||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TReminderServiceFactory } from "../reminder/reminder-types";
|
||||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
@ -131,6 +132,7 @@ type TSecretServiceFactoryDep = {
|
|||||||
"insertMany" | "insertApprovalSecretTags"
|
"insertMany" | "insertApprovalSecretTags"
|
||||||
>;
|
>;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
reminderService: Pick<TReminderServiceFactory, "createReminder">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
|
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
|
||||||
@ -153,7 +155,8 @@ export const secretServiceFactory = ({
|
|||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
secretV2BridgeService,
|
secretV2BridgeService,
|
||||||
secretApprovalRequestService,
|
secretApprovalRequestService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
reminderService
|
||||||
}: TSecretServiceFactoryDep) => {
|
}: TSecretServiceFactoryDep) => {
|
||||||
const getSecretReference = async (projectId: string) => {
|
const getSecretReference = async (projectId: string) => {
|
||||||
// if bot key missing means e2e still exist
|
// if bot key missing means e2e still exist
|
||||||
@ -546,7 +549,8 @@ export const secretServiceFactory = ({
|
|||||||
await secretQueueService.removeSecretReminder(
|
await secretQueueService.removeSecretReminder(
|
||||||
{
|
{
|
||||||
repeatDays: secret.secretReminderRepeatDays,
|
repeatDays: secret.secretReminderRepeatDays,
|
||||||
secretId: secret.id
|
secretId: secret.id,
|
||||||
|
projectId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -1072,7 +1076,8 @@ export const secretServiceFactory = ({
|
|||||||
await secretQueueService.removeSecretReminder(
|
await secretQueueService.removeSecretReminder(
|
||||||
{
|
{
|
||||||
repeatDays: secret.secretReminderRepeatDays,
|
repeatDays: secret.secretReminderRepeatDays,
|
||||||
secretId: secret.id
|
secretId: secret.id,
|
||||||
|
projectId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -1660,8 +1665,6 @@ export const secretServiceFactory = ({
|
|||||||
secretComment,
|
secretComment,
|
||||||
secretValue,
|
secretValue,
|
||||||
tagIds,
|
tagIds,
|
||||||
reminderNote: secretReminderNote,
|
|
||||||
reminderRepeatDays: secretReminderRepeatDays,
|
|
||||||
secretMetadata
|
secretMetadata
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1824,9 +1827,6 @@ export const secretServiceFactory = ({
|
|||||||
secretComment,
|
secretComment,
|
||||||
secretValue,
|
secretValue,
|
||||||
tagIds,
|
tagIds,
|
||||||
reminderNote: secretReminderNote,
|
|
||||||
reminderRepeatDays: secretReminderRepeatDays,
|
|
||||||
secretReminderRecipients,
|
|
||||||
secretMetadata
|
secretMetadata
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1835,9 +1835,6 @@ export const secretServiceFactory = ({
|
|||||||
return { type: SecretProtectionType.Approval as const, approval };
|
return { type: SecretProtectionType.Approval as const, approval };
|
||||||
}
|
}
|
||||||
const secret = await secretV2BridgeService.updateSecret({
|
const secret = await secretV2BridgeService.updateSecret({
|
||||||
secretReminderRepeatDays,
|
|
||||||
secretReminderNote,
|
|
||||||
secretReminderRecipients,
|
|
||||||
skipMultilineEncoding,
|
skipMultilineEncoding,
|
||||||
tagIds,
|
tagIds,
|
||||||
secretComment,
|
secretComment,
|
||||||
@ -1855,6 +1852,21 @@ export const secretServiceFactory = ({
|
|||||||
secretValue,
|
secretValue,
|
||||||
secretMetadata
|
secretMetadata
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (secretReminderRepeatDays) {
|
||||||
|
await reminderService.createReminder({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
reminder: {
|
||||||
|
secretId: secret.id,
|
||||||
|
message: secretReminderNote,
|
||||||
|
repeatDays: secretReminderRepeatDays,
|
||||||
|
recipients: secretReminderRecipients
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return { type: SecretProtectionType.Direct as const, secret };
|
return { type: SecretProtectionType.Direct as const, secret };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2246,6 +2258,29 @@ export const secretServiceFactory = ({
|
|||||||
secrets: inputSecrets,
|
secrets: inputSecrets,
|
||||||
mode
|
mode
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
inputSecrets
|
||||||
|
.filter((el) => el.secretReminderRepeatDays)
|
||||||
|
.map(async (secret) => {
|
||||||
|
await reminderService.createReminder({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
reminder: {
|
||||||
|
secretId: secrets.find(
|
||||||
|
(el) =>
|
||||||
|
(el.secretKey === secret.secretKey || el.secretKey === secret.newSecretName) &&
|
||||||
|
el.secretPath === (secret.secretPath || secretPath)
|
||||||
|
)?.id,
|
||||||
|
message: secret.secretReminderNote,
|
||||||
|
repeatDays: secret.secretReminderRepeatDays
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return { type: SecretProtectionType.Direct as const, secrets };
|
return { type: SecretProtectionType.Direct as const, secrets };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,6 +310,7 @@ export type TUpdateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
|
|||||||
secretMetadata?: ResourceMetadataDTO;
|
secretMetadata?: ResourceMetadataDTO;
|
||||||
secretReminderRepeatDays?: number | null;
|
secretReminderRepeatDays?: number | null;
|
||||||
secretReminderNote?: string | null;
|
secretReminderNote?: string | null;
|
||||||
|
secretPath?: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -410,6 +411,7 @@ export type TCreateSecretReminderDTO = {
|
|||||||
oldSecret: TPartialSecret;
|
oldSecret: TPartialSecret;
|
||||||
newSecret: TPartialSecret;
|
newSecret: TPartialSecret;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
secretReminderRecipients: string[];
|
||||||
|
|
||||||
deleteRecipients?: boolean;
|
deleteRecipients?: boolean;
|
||||||
};
|
};
|
||||||
@ -417,6 +419,7 @@ export type TCreateSecretReminderDTO = {
|
|||||||
export type TRemoveSecretReminderDTO = {
|
export type TRemoveSecretReminderDTO = {
|
||||||
secretId: string;
|
secretId: string;
|
||||||
repeatDays: number;
|
repeatDays: number;
|
||||||
|
projectId: string;
|
||||||
deleteRecipients?: boolean;
|
deleteRecipients?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
|||||||
popUpProps: PopoverProps;
|
popUpProps: PopoverProps;
|
||||||
popUpContentProps: PopoverContentProps;
|
popUpContentProps: PopoverContentProps;
|
||||||
dateFormat?: "PPP" | "PP" | "P"; // extend as needed
|
dateFormat?: "PPP" | "PP" | "P"; // extend as needed
|
||||||
|
hideTime?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Doc: https://react-day-picker.js.org/
|
// Doc: https://react-day-picker.js.org/
|
||||||
@ -27,6 +28,7 @@ export const DatePicker = ({
|
|||||||
popUpProps,
|
popUpProps,
|
||||||
popUpContentProps,
|
popUpContentProps,
|
||||||
dateFormat = "PPP",
|
dateFormat = "PPP",
|
||||||
|
hideTime = false,
|
||||||
...props
|
...props
|
||||||
}: DatePickerProps) => {
|
}: DatePickerProps) => {
|
||||||
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
||||||
@ -85,6 +87,7 @@ export const DatePicker = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{!hideTime && (
|
||||||
<div className="mx-4 my-4">
|
<div className="mx-4 my-4">
|
||||||
<Input
|
<Input
|
||||||
type="time"
|
type="time"
|
||||||
@ -93,6 +96,7 @@ export const DatePicker = ({
|
|||||||
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
1
frontend/src/hooks/api/reminders/index.tsx
Normal file
1
frontend/src/hooks/api/reminders/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { useCreateReminder, useDeleteReminder, useGetReminder } from "./queries";
|
60
frontend/src/hooks/api/reminders/queries.tsx
Normal file
60
frontend/src/hooks/api/reminders/queries.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { CreateReminderDTO, DeleteReminderDTO, Reminder } from "./types";
|
||||||
|
|
||||||
|
export const reminderKeys = {
|
||||||
|
getReminder: (secretId: string) => ["get-reminder", secretId] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateReminder = (secretId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<Reminder, object, CreateReminderDTO>({
|
||||||
|
mutationFn: async ({ message, repeatDays, nextReminderDate, recipients }) => {
|
||||||
|
const { data } = await apiRequest.post<{ reminder: Reminder }>(
|
||||||
|
`/api/v1/reminders/secrets/${secretId}`,
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
repeatDays,
|
||||||
|
nextReminderDate,
|
||||||
|
recipients
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data.reminder;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteReminder = (secretId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<Reminder, object, DeleteReminderDTO>({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const { data } = await apiRequest.delete<{ reminder: Reminder }>(
|
||||||
|
`/api/v1/reminders/secrets/${secretId}`
|
||||||
|
);
|
||||||
|
return data.reminder;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetReminder = (secretId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: reminderKeys.getReminder(secretId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ reminder: Reminder }>(
|
||||||
|
`/api/v1/reminders/secrets/${secretId}`
|
||||||
|
);
|
||||||
|
return data.reminder;
|
||||||
|
},
|
||||||
|
enabled: Boolean(secretId)
|
||||||
|
});
|
||||||
|
};
|
14
frontend/src/hooks/api/reminders/types.ts
Normal file
14
frontend/src/hooks/api/reminders/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export type CreateReminderDTO = {
|
||||||
|
message?: string | null;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
nextReminderDate?: Date | null;
|
||||||
|
secretId: string;
|
||||||
|
recipients?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteReminderDTO = {
|
||||||
|
secretId: string;
|
||||||
|
reminderId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Reminder = { id: string } & CreateReminderDTO;
|
@ -1,25 +1,65 @@
|
|||||||
import { useEffect } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faClock, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faClock, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DatePicker,
|
||||||
FilterableSelect,
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
TextArea
|
TextArea
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
||||||
|
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||||
|
import { useCreateReminder, useDeleteReminder } from "@app/hooks/api/reminders";
|
||||||
|
import { reminderKeys } from "@app/hooks/api/reminders/queries";
|
||||||
|
import { Reminder } from "@app/hooks/api/reminders/types";
|
||||||
|
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const MIN_REPEAT_DAYS = 1;
|
||||||
|
const MAX_REPEAT_DAYS = 365;
|
||||||
|
const DEFAULT_REPEAT_DAYS = 30;
|
||||||
|
const DEFAULT_TEXTAREA_ROWS = 8;
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
enum ReminderType {
|
||||||
|
Recurring = "Recurring",
|
||||||
|
OneTime = "One Time"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types
|
||||||
|
interface RecipientOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReminderFormProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
reminderId?: string;
|
||||||
|
onOpenChange: () => void;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
secretId: string;
|
||||||
|
reminder?: Reminder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation Schema
|
||||||
const ReminderFormSchema = z.object({
|
const ReminderFormSchema = z.object({
|
||||||
note: z.string().optional().nullable(),
|
message: z.string().optional().nullable(),
|
||||||
recipients: z
|
recipients: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@ -28,84 +68,266 @@ const ReminderFormSchema = z.object({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
days: z
|
repeatDays: z
|
||||||
.number()
|
.number()
|
||||||
.min(1, { message: "Must be at least 1 day" })
|
.min(MIN_REPEAT_DAYS, { message: `Must be at least ${MIN_REPEAT_DAYS} day` })
|
||||||
.max(365, { message: "Must be less than 365 days" })
|
.max(MAX_REPEAT_DAYS, { message: `Must be less than ${MAX_REPEAT_DAYS} days` })
|
||||||
.nullable()
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
nextReminderDate: z.coerce
|
||||||
|
.date()
|
||||||
|
.refine((data) => data > new Date(), { message: "Reminder date must be in the future" })
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
reminderType: z.enum(["Recurring", "One Time"])
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
||||||
|
|
||||||
interface ReminderFormProps {
|
// Custom hook for form state management
|
||||||
isOpen: boolean;
|
const useReminderForm = (reminderData?: Reminder) => {
|
||||||
repeatDays?: number | null;
|
const { repeatDays, message, nextReminderDate } = reminderData || {};
|
||||||
note?: string | null;
|
|
||||||
recipients?: string[] | null;
|
|
||||||
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const isEditMode = Boolean(reminderData);
|
||||||
|
|
||||||
|
const defaultValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
repeatDays: repeatDays || null,
|
||||||
|
message: message || "",
|
||||||
|
nextReminderDate: nextReminderDate || null,
|
||||||
|
reminderType: repeatDays ? ReminderType.Recurring : ReminderType.OneTime,
|
||||||
|
recipients: []
|
||||||
|
}),
|
||||||
|
[repeatDays, message, nextReminderDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isEditMode,
|
||||||
|
reminderData,
|
||||||
|
defaultValues
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom hook for workspace members
|
||||||
|
const useWorkspaceMembers = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace?.id);
|
||||||
|
|
||||||
|
const memberOptions = useMemo(
|
||||||
|
(): RecipientOption[] =>
|
||||||
|
members.map((member) => ({
|
||||||
|
label: member.user.username || member.user.email,
|
||||||
|
value: member.user.id
|
||||||
|
})),
|
||||||
|
[members]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { members, memberOptions };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main component
|
||||||
export const CreateReminderForm = ({
|
export const CreateReminderForm = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
repeatDays,
|
workspaceId,
|
||||||
note,
|
environment,
|
||||||
recipients
|
secretPath,
|
||||||
|
secretId,
|
||||||
|
reminder
|
||||||
}: ReminderFormProps) => {
|
}: ReminderFormProps) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const queryClient = useQueryClient();
|
||||||
|
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
||||||
|
|
||||||
const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace?.id);
|
// Custom hooks
|
||||||
|
const { isEditMode, reminderData } = useReminderForm(reminder);
|
||||||
|
const { memberOptions } = useWorkspaceMembers();
|
||||||
|
|
||||||
|
// API mutations
|
||||||
|
const { mutateAsync: createReminder } = useCreateReminder(secretId);
|
||||||
|
const { mutateAsync: deleteReminder } = useDeleteReminder(secretId);
|
||||||
|
|
||||||
|
// Form setup
|
||||||
|
const form = useForm<TReminderFormSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
repeatDays: reminderData?.repeatDays || null,
|
||||||
|
message: reminderData?.message || "",
|
||||||
|
nextReminderDate: reminderData?.nextReminderDate || null,
|
||||||
|
reminderType: reminderData?.repeatDays ? ReminderType.Recurring : ReminderType.OneTime,
|
||||||
|
recipients: []
|
||||||
|
},
|
||||||
|
resolver: zodResolver(ReminderFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TReminderFormSchema>({
|
} = form;
|
||||||
defaultValues: {
|
|
||||||
days: repeatDays || undefined,
|
|
||||||
note: note || ""
|
|
||||||
},
|
|
||||||
resolver: zodResolver(ReminderFormSchema)
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
// Watch form values
|
||||||
onOpenChange(false, data);
|
const reminderType = watch("reminderType");
|
||||||
|
|
||||||
|
// Invalidate queries helper
|
||||||
|
const invalidateQueries = () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: dashboardKeys.getDashboardSecrets({
|
||||||
|
projectId: workspaceId,
|
||||||
|
secretPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: reminderKeys.getReminder(secretId)
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Form submission handler
|
||||||
// On initial load, filter the members to only include the recipients
|
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
||||||
if (members.length) {
|
try {
|
||||||
const filteredMembers = members.filter((m) => recipients?.find((r) => r === m.user.id));
|
await createReminder({
|
||||||
setValue(
|
repeatDays: data.repeatDays,
|
||||||
"recipients",
|
message: data.message,
|
||||||
filteredMembers.map((m) => ({
|
recipients: data.recipients?.map((r) => r.value) || [],
|
||||||
label: m.user.username || m.user.email,
|
secretId,
|
||||||
value: m.user.id
|
nextReminderDate: data.nextReminderDate
|
||||||
}))
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [members, isOpen, recipients]);
|
|
||||||
|
|
||||||
|
invalidateQueries();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: `Successfully ${isEditMode ? "updated" : "created"} secret reminder`
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
onOpenChange();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save reminder:", error);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to save reminder. Please try again."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete reminder handler
|
||||||
|
const handleDeleteReminder = async () => {
|
||||||
|
try {
|
||||||
|
await deleteReminder({ reminderId: reminder?.id || "", secretId });
|
||||||
|
invalidateQueries();
|
||||||
|
reset();
|
||||||
|
onOpenChange();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully deleted reminder"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete reminder:", error);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to delete reminder. Please try again."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle reminder type change
|
||||||
|
const handleReminderTypeChange = useCallback(
|
||||||
|
(newType: string) => {
|
||||||
|
if (newType === ReminderType.Recurring) {
|
||||||
|
setValue("repeatDays", DEFAULT_REPEAT_DAYS);
|
||||||
|
setValue("nextReminderDate", null);
|
||||||
|
} else if (newType === ReminderType.OneTime) {
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
setValue("nextReminderDate", tomorrow);
|
||||||
|
setValue("repeatDays", null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize form with existing data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (repeatDays) setValue("days", repeatDays);
|
if (!reminderData) return;
|
||||||
if (note) setValue("note", note);
|
|
||||||
}, [repeatDays, note]);
|
const {
|
||||||
|
repeatDays: repeatDaysInitial,
|
||||||
|
message,
|
||||||
|
recipients,
|
||||||
|
nextReminderDate: nextReminderDateInitial
|
||||||
|
} = reminderData;
|
||||||
|
|
||||||
|
if (repeatDaysInitial) {
|
||||||
|
setValue("repeatDays", repeatDaysInitial);
|
||||||
|
setValue("reminderType", ReminderType.Recurring);
|
||||||
|
} else {
|
||||||
|
setValue("reminderType", ReminderType.OneTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) setValue("message", message);
|
||||||
|
if (nextReminderDateInitial) setValue("nextReminderDate", nextReminderDateInitial);
|
||||||
|
|
||||||
|
// Set recipients
|
||||||
|
if (recipients?.length && memberOptions.length) {
|
||||||
|
const selectedRecipients = memberOptions.filter((option) =>
|
||||||
|
recipients.includes(option.value)
|
||||||
|
);
|
||||||
|
setValue("recipients", selectedRecipients);
|
||||||
|
}
|
||||||
|
}, [reminderData, memberOptions, setValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`${repeatDays ? "Update" : "Create"} reminder`}
|
title={`${isEditMode ? "Update" : "Create"} reminder`}
|
||||||
subTitle="Set up a reminder for when this secret should be rotated. Everyone with access to this project will be notified when the reminder is triggered."
|
subTitle="Set up a reminder for when this secret should be rotated. Everyone with access to this project will be notified when the reminder is triggered."
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
||||||
<div className="space-y-2">
|
{/* Reminder Type Selection */}
|
||||||
<div>
|
<Controller
|
||||||
|
name="reminderType"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Recurrence"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
onChange(val);
|
||||||
|
handleReminderTypeChange(val);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500 capitalize"
|
||||||
|
position="popper"
|
||||||
|
placeholder="Select reminder recurrence"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
>
|
||||||
|
{Object.values(ReminderType).map((type) => (
|
||||||
|
<SelectItem value={type} key={type}>
|
||||||
|
{type}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Conditional Fields Based on Reminder Type */}
|
||||||
|
{reminderType === ReminderType.Recurring ? (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="days"
|
name="repeatDays"
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<>
|
<div>
|
||||||
<FormControl
|
<FormControl
|
||||||
isRequired
|
isRequired
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
@ -114,13 +336,19 @@ export const CreateReminderForm = ({
|
|||||||
errorText={fieldState.error?.message || ""}
|
errorText={fieldState.error?.message || ""}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
|
onChange={(el) => {
|
||||||
|
const value = parseInt(el.target.value, 10);
|
||||||
|
setValue("repeatDays", Number.isNaN(value) ? null : value);
|
||||||
|
}}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="31"
|
placeholder={DEFAULT_REPEAT_DAYS.toString()}
|
||||||
defaultValue={repeatDays || undefined}
|
value={field.value || ""}
|
||||||
value={field.value || undefined}
|
min={MIN_REPEAT_DAYS}
|
||||||
|
max={MAX_REPEAT_DAYS}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
{/* Interval description */}
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"ml-1 mt-2 text-xs",
|
"ml-1 mt-2 text-xs",
|
||||||
@ -130,23 +358,54 @@ export const CreateReminderForm = ({
|
|||||||
A reminder will be sent every{" "}
|
A reminder will be sent every{" "}
|
||||||
{field.value && field.value > 1 ? `${field.value} days` : "day"}
|
{field.value && field.value > 1 ? `${field.value} days` : "day"}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="nextReminderDate"
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<div>
|
||||||
|
<FormControl
|
||||||
|
isRequired
|
||||||
|
className="mb-0"
|
||||||
|
label="Reminder Date"
|
||||||
|
isError={Boolean(fieldState.error)}
|
||||||
|
errorText={fieldState.error?.message || ""}
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
value={field.value || undefined}
|
||||||
|
className="w-full"
|
||||||
|
onChange={field.onChange}
|
||||||
|
dateFormat="P"
|
||||||
|
popUpProps={{
|
||||||
|
open: isDatePickerOpen,
|
||||||
|
onOpenChange: setIsDatePickerOpen
|
||||||
|
}}
|
||||||
|
popUpContentProps={{}}
|
||||||
|
hideTime
|
||||||
|
hidden={{ before: new Date(Date.now() + 86400000) }}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Message/Note Field */}
|
||||||
<FormControl label="Note" className="mb-0">
|
<FormControl label="Note" className="mb-0">
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="Remember to rotate the AWS secret every month."
|
placeholder="Remember to rotate the AWS secret every month."
|
||||||
className="border border-mineshaft-600 text-sm"
|
className="border border-mineshaft-600 text-sm"
|
||||||
rows={8}
|
rows={DEFAULT_TEXTAREA_ROWS}
|
||||||
defaultValue={note || ""}
|
|
||||||
reSize="none"
|
reSize="none"
|
||||||
cols={30}
|
cols={30}
|
||||||
{...register("note")}
|
{...register("message")}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
{/* Recipients Selection */}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="recipients"
|
name="recipients"
|
||||||
@ -163,47 +422,50 @@ export const CreateReminderForm = ({
|
|||||||
className="mb-0"
|
className="mb-0"
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
|
menuPlacement="top"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="Select reminder recipients..."
|
placeholder="Select reminder recipients..."
|
||||||
isMulti
|
isMulti
|
||||||
name="recipients"
|
name="recipients"
|
||||||
options={members.map((member) => ({
|
options={memberOptions}
|
||||||
label: member.user.username || member.user.email,
|
|
||||||
value: member.user.id
|
|
||||||
}))}
|
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="mt-7 flex items-center space-x-4">
|
{/* Action Buttons */}
|
||||||
|
<div className="flex items-center space-x-4 pt-4">
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
key="layout-create-project-submit"
|
|
||||||
className=""
|
className=""
|
||||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{repeatDays ? "Update" : "Create"} reminder
|
{isEditMode ? "Update" : "Create"} reminder
|
||||||
</Button>
|
</Button>
|
||||||
{repeatDays && (
|
|
||||||
|
{isEditMode && (
|
||||||
<Button
|
<Button
|
||||||
key="layout-cancel-create-project"
|
onClick={handleDeleteReminder}
|
||||||
onClick={() => onOpenChange(false, { days: null, note: null })}
|
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
|
variant="outline"
|
||||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
type="button"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Delete reminder
|
Delete reminder
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
key="layout-cancel-create-project"
|
onClick={onOpenChange}
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
|
type="button"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
|
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
|
||||||
@ -55,6 +55,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo
|
|||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useGetSecretVersion } from "@app/hooks/api";
|
import { useGetSecretVersion } from "@app/hooks/api";
|
||||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||||
|
import { useGetReminder } from "@app/hooks/api/reminders";
|
||||||
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
@ -114,6 +115,7 @@ export const SecretDetailSidebar = ({
|
|||||||
|
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: reminderData } = useGetReminder(secret?.id);
|
||||||
|
|
||||||
const tagFields = useFieldArray({
|
const tagFields = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@ -220,23 +222,8 @@ export const SecretDetailSidebar = ({
|
|||||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReminderSubmit = async (
|
|
||||||
reminderRepeatDays: number | null | undefined,
|
|
||||||
reminderNote: string | null | undefined,
|
|
||||||
reminderRecipients: string[] | undefined
|
|
||||||
) => {
|
|
||||||
await onSaveSecret(
|
|
||||||
secret,
|
|
||||||
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true, reminderRecipients },
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||||
|
|
||||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
|
||||||
const secretReminderNote = watch("reminderNote");
|
|
||||||
const secretReminderRecipients = watch("reminderRecipients");
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(
|
setValue(
|
||||||
"reminderRecipients",
|
"reminderRecipients",
|
||||||
@ -302,27 +289,33 @@ export const SecretDetailSidebar = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDaysUntilReminder = useMemo(() => {
|
||||||
|
return (): string => {
|
||||||
|
const now = new Date();
|
||||||
|
now.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const target = new Date(reminderData?.nextReminderDate || "");
|
||||||
|
target.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const diffTime = target.getTime() - now.getTime();
|
||||||
|
const daysRemaining = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
return `Days until next reminder: ${daysRemaining}`;
|
||||||
|
};
|
||||||
|
}, [reminderData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateReminderForm
|
<CreateReminderForm
|
||||||
repeatDays={secretReminderRepeatDays}
|
|
||||||
note={secretReminderNote}
|
|
||||||
recipients={secretReminderRecipients}
|
|
||||||
isOpen={createReminderFormOpen}
|
isOpen={createReminderFormOpen}
|
||||||
onOpenChange={(_, data) => {
|
onOpenChange={() => {
|
||||||
setCreateReminderFormOpen.toggle();
|
setCreateReminderFormOpen.toggle();
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const recipients = data.recipients?.length
|
|
||||||
? data.recipients.map((recipient) => recipient.value)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
|
|
||||||
setValue("reminderNote", data.note, { shouldDirty: false });
|
|
||||||
setValue("reminderRecipients", recipients, { shouldDirty: false });
|
|
||||||
handleReminderSubmit(data.days, data.note, recipients);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
workspaceId={currentWorkspace.id}
|
||||||
|
environment={environment}
|
||||||
|
secretPath={secretPath}
|
||||||
|
secretId={secret?.id}
|
||||||
|
reminder={reminderData}
|
||||||
/>
|
/>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.secretAccessUpgradePlan.isOpen}
|
isOpen={popUp.secretAccessUpgradePlan.isOpen}
|
||||||
@ -734,14 +727,11 @@ export const SecretDetailSidebar = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
{reminderData && reminderData.nextReminderDate ? (
|
||||||
<div className="flex items-center justify-between px-2">
|
<div className="flex items-center justify-between px-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
||||||
<span className="text-sm text-bunker-300">
|
<span className="text-sm text-bunker-300">{getDaysUntilReminder()}</span>
|
||||||
Reminder every {secretReminderRepeatDays}{" "}
|
|
||||||
{secretReminderRepeatDays > 1 ? "days" : "day"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
Reference in New Issue
Block a user