From 9f1ac77afab4a87ccbdaecf2c9e3e486f4239d5e Mon Sep 17 00:00:00 2001 From: x Date: Thu, 1 May 2025 16:34:29 -0400 Subject: [PATCH 01/16] invalidate cache --- backend/e2e-test/mocks/keystore.ts | 12 +++ backend/src/keystore/keystore.ts | 28 +++++ backend/src/keystore/memory.ts | 13 +++ backend/src/server/config/rateLimiter.ts | 7 ++ backend/src/server/routes/v1/admin-router.ts | 38 ++++++- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 2 +- .../super-admin/super-admin-service.ts | 10 +- .../services/super-admin/super-admin-types.ts | 5 + .../src/services/telemetry/telemetry-types.ts | 11 +- frontend/src/hooks/api/admin/index.ts | 1 + frontend/src/hooks/api/admin/mutation.ts | 13 +++ frontend/src/hooks/api/admin/queries.ts | 3 +- frontend/src/hooks/api/admin/types.ts | 9 ++ .../layouts/ProjectLayout/ProjectLayout.tsx | 2 +- .../pages/admin/OverviewPage/OverviewPage.tsx | 8 +- .../OverviewPage/components/CachingPanel.tsx | 100 ++++++++++++++++++ 16 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts index 48f52f9e7..ddf57bfd5 100644 --- a/backend/e2e-test/mocks/keystore.ts +++ b/backend/e2e-test/mocks/keystore.ts @@ -1,5 +1,6 @@ import { TKeyStoreFactory } from "@app/keystore/keystore"; import { Lock } from "@app/lib/red-lock"; +import RE2 from "re2"; export const mockKeyStore = (): TKeyStoreFactory => { const store: Record = {}; @@ -18,6 +19,17 @@ export const mockKeyStore = (): TKeyStoreFactory => { delete store[key]; return 1; }, + deleteItems: async (pattern) => { + const regex = new RE2(pattern.replace(/\*/g, ".*")); + let deletedCount = 0; + for (const key of Object.keys(store)) { + if (regex.test(key)) { + delete store[key]; + deletedCount += 1; + } + } + return deletedCount; + }, getItem: async (key) => { const value = store[key]; if (typeof value === "string") { diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index ac28e9ade..9371cebb8 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -57,6 +57,8 @@ type TWaitTillReady = { jitter?: number; }; +const DELETION_BATCH_SIZE = 500; + export const keyStoreFactory = (redisUrl: string) => { const redis = new Redis(redisUrl); const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 }); @@ -75,6 +77,31 @@ export const keyStoreFactory = (redisUrl: string) => { const deleteItem = async (key: string) => redis.del(key); + const deleteItems = async (pattern: string) => { + let cursor = "0"; + let totalDeleted = 0; + + do { + // Await in loop is needed so that Redis is not overwhelmed + // eslint-disable-next-line no-await-in-loop + const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads + cursor = nextCursor; + + for (let i = 0; i < keys.length; i += DELETION_BATCH_SIZE) { + const batch = keys.slice(i, i + DELETION_BATCH_SIZE); + const pipeline = redis.pipeline(); + for (const key of batch) { + pipeline.unlink(key); + } + // eslint-disable-next-line no-await-in-loop + await pipeline.exec(); + totalDeleted += batch.length; + } + } while (cursor !== "0"); + + return totalDeleted; + }; + const incrementBy = async (key: string, value: number) => redis.incrby(key, value); const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds); @@ -108,6 +135,7 @@ export const keyStoreFactory = (redisUrl: string) => { setExpiry, setItemWithExpiry, deleteItem, + deleteItems, incrementBy, acquireLock(resources: string[], duration: number, settings?: Partial) { return redisLock.acquire(resources, duration, settings); diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts index 10b28ffec..55699207f 100644 --- a/backend/src/keystore/memory.ts +++ b/backend/src/keystore/memory.ts @@ -1,3 +1,5 @@ +import RE2 from "re2"; + import { Lock } from "@app/lib/red-lock"; import { TKeyStoreFactory } from "./keystore"; @@ -19,6 +21,17 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => { delete store[key]; return 1; }, + deleteItems: async (pattern) => { + const regex = new RE2(pattern.replace(/\*/g, ".*")); + let deletedCount = 0; + for (const key of Object.keys(store)) { + if (regex.test(key)) { + delete store[key]; + deletedCount += 1; + } + } + return deletedCount; + }, getItem: async (key) => { const value = store[key]; if (typeof value === "string") { diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 681442d1b..42bf37c71 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -100,3 +100,10 @@ export const publicSshCaLimit: RateLimitOptions = { max: 30, // conservative default keyGenerator: (req) => req.realIp }; + +export const invalidateCacheLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + hook: "preValidation", + max: 1, + keyGenerator: (req) => req.realIp +}; diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 6eb1804f1..c1dbe3805 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -4,13 +4,14 @@ import { z } from "zod"; import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; -import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; -import { LoginMethod } from "@app/services/super-admin/super-admin-types"; +import { CacheType, LoginMethod } from "@app/services/super-admin/super-admin-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerAdminRouter = async (server: FastifyZodProvider) => { @@ -535,4 +536,37 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "POST", + url: "/invalidate-cache", + config: { + rateLimit: invalidateCacheLimit + }, + schema: { + body: z.object({ + type: z.nativeEnum(CacheType) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.superAdmin.invalidateCache(req.body.type); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.InvalidateCache, + distinctId: getTelemetryDistinctId(req), + properties: { + ...req.auditLogInfo + } + }); + + return { + message: "Successfully purged cache" + }; + } + }); }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 6ab348520..cd2773172 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -7,6 +7,7 @@ import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecre import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { generateCacheKeyFromData } from "@app/lib/crypto/cache"; +import { applyJitter } from "@app/lib/dates"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { buildFindFilter, @@ -22,7 +23,6 @@ import type { TFindSecretsByFolderIdsFilter, TGetSecretsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; -import { applyJitter } from "@app/lib/dates"; export const SecretServiceCacheKeys = { get productKey() { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 317348cac..f993d2398 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -26,6 +26,7 @@ import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { + CacheType, LoginMethod, TAdminBootstrapInstanceDTO, TAdminGetIdentitiesDTO, @@ -45,7 +46,7 @@ type TSuperAdminServiceFactoryDep = { kmsService: Pick; kmsRootConfigDAL: TKmsRootConfigDALFactory; orgService: Pick; - keyStore: Pick; + keyStore: Pick; licenseService: Pick; }; @@ -570,6 +571,10 @@ export const superAdminServiceFactory = ({ await kmsService.updateEncryptionStrategy(strategy); }; + const invalidateCache = async (type: CacheType) => { + if (type === CacheType.ALL || type === CacheType.SECRETS) await keyStore.deleteItems("secret-manager:*"); + }; + return { initServerCfg, updateServerCfg, @@ -583,6 +588,7 @@ export const superAdminServiceFactory = ({ getConfiguredEncryptionStrategies, grantServerAdminAccessToUser, deleteIdentitySuperAdminAccess, - deleteUserSuperAdminAccess + deleteUserSuperAdminAccess, + invalidateCache }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 64ec92632..c804bed74 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -44,3 +44,8 @@ export enum LoginMethod { LDAP = "ldap", OIDC = "oidc" } + +export enum CacheType { + ALL = "all", + SECRETS = "secrets" +} diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index ab90a71d4..9e046cdbd 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -21,7 +21,8 @@ export enum PostHogEventTypes { IssueSshHostUserCert = "Issue SSH Host User Certificate", IssueSshHostHostCert = "Issue SSH Host Host Certificate", SignCert = "Sign PKI Certificate", - IssueCert = "Issue PKI Certificate" + IssueCert = "Issue PKI Certificate", + InvalidateCache = "Invalidate Cache" } export type TSecretModifiedEvent = { @@ -203,6 +204,13 @@ export type TIssueCertificateEvent = { }; }; +export type TInvalidateCacheEvent = { + event: PostHogEventTypes.InvalidateCache; + properties: { + userAgent?: string; + }; +}; + export type TPostHogEvent = { distinctId: string } & ( | TSecretModifiedEvent | TAdminInitEvent @@ -221,4 +229,5 @@ export type TPostHogEvent = { distinctId: string } & ( | TIssueSshHostHostCertEvent | TSignCertificateEvent | TIssueCertificateEvent + | TInvalidateCacheEvent ); diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index d43fbc080..bf2cd4345 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -3,6 +3,7 @@ export { useAdminGrantServerAdminAccess, useAdminRemoveIdentitySuperAdminAccess, useCreateAdminUser, + useInvalidateCache, useRemoveUserServerAdminAccess, useUpdateAdminSlackConfig, useUpdateServerConfig, diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index b3e1e37b4..78d6edc9a 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -9,6 +9,7 @@ import { AdminSlackConfig, RootKeyEncryptionStrategy, TCreateAdminUserDTO, + TInvalidateCacheDTO, TServerConfig, TUpdateAdminSlackConfigDTO } from "./types"; @@ -147,3 +148,15 @@ export const useUpdateServerEncryptionStrategy = () => { } }); }; + +export const useInvalidateCache = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (dto) => { + await apiRequest.post("/api/v1/admin/invalidate-cache", dto); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInvalidateCache() }); + } + }); +}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index b24841dbd..a1a00ee6a 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -23,7 +23,8 @@ export const adminQueryKeys = { getIdentities: (filters: AdminGetIdentitiesFilters) => [adminStandaloneKeys.getIdentities, { filters }] as const, getAdminSlackConfig: () => ["admin-slack-config"] as const, - getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const + getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const, + getInvalidateCache: () => ["admin-invalidate-cache"] as const }; export const fetchServerConfig = async () => { diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 11f2cf44f..c3cf433fd 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -74,3 +74,12 @@ export enum RootKeyEncryptionStrategy { Software = "SOFTWARE", HSM = "HSM" } + +export enum CacheType { + ALL = "all", + SECRETS = "secrets" +} + +export type TInvalidateCacheDTO = { + type: CacheType; +}; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index a862c31a3..8533d7007 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -13,9 +13,9 @@ import { TBreadcrumbFormat } from "@app/components/v2"; import { - useProjectPermission, ProjectPermissionActions, ProjectPermissionSub, + useProjectPermission, useSubscription, useWorkspace } from "@app/context"; diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index a9242c252..1f1f0760c 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -31,6 +31,7 @@ import { import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel"; import { AuthPanel } from "./components/AuthPanel"; +import { CachingPanel } from "./components/CachingPanel"; import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; import { UserPanel } from "./components/UserPanel"; @@ -42,7 +43,8 @@ enum TabSections { Integrations = "integrations", Users = "users", Identities = "identities", - Kmip = "kmip" + Kmip = "kmip", + Caching = "caching" } enum SignUpModes { @@ -164,6 +166,7 @@ export const OverviewPage = () => { Integrations User Identities Machine Identities + Caching @@ -408,6 +411,9 @@ export const OverviewPage = () => { + + + )} diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx new file mode 100644 index 000000000..a30042999 --- /dev/null +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -0,0 +1,100 @@ +import { useState } from "react"; + +import { createNotification } from "@app/components/notifications"; +import { Button, DeleteActionModal } from "@app/components/v2"; +import { useOrgPermission } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useInvalidateCache } from "@app/hooks/api"; +import { CacheType } from "@app/hooks/api/admin/types"; + +export const CachingPanel = () => { + const { mutateAsync: invalidateCache } = useInvalidateCache(); + const { membership } = useOrgPermission(); + + const [type, setType] = useState(CacheType.ALL); + const [isLoading, setIsLoading] = useState(false); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "invalidateCache" + ] as const); + + const handleInvalidateCacheSubmit = async () => { + try { + setIsLoading(true); + + await invalidateCache({ type }); + + createNotification({ + text: `Successfully purged ${type} cache`, + type: "success" + }); + + setIsLoading(false); + handlePopUpClose("invalidateCache"); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to purge ${type} cache`, + type: "error" + }); + } + }; + + return ( + <> +
+
+ Secrets Cache + + The secrets cache encompasses all secrets stored within the system and provides a + temporary, secure storage location for frequently accessed credentials. + +
+ + +
+ + {/* Uncomment this when we have more than one cache type */} + {/*
+
+ All Cache + + All cache refers to the entirety of cached data throughout the system, including secrets + and miscellaneous information. + +
+ + +
*/} + + handlePopUpToggle("invalidateCache", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleInvalidateCacheSubmit} + /> + + ); +}; From 346d2f213efc935c8a6335743bd14afb29712c36 Mon Sep 17 00:00:00 2001 From: x Date: Thu, 1 May 2025 17:33:24 -0400 Subject: [PATCH 02/16] improvements + review fixes --- backend/e2e-test/mocks/keystore.ts | 32 +++++++++++++------ backend/src/keystore/keystore.ts | 19 ++++++++--- backend/src/keystore/memory.ts | 27 +++++++++++----- backend/src/lib/delay/index.ts | 4 +++ backend/src/server/routes/v1/admin-router.ts | 10 ++++-- .../super-admin/super-admin-service.ts | 7 +++- .../src/services/telemetry/telemetry-types.ts | 1 + .../OverviewPage/components/CachingPanel.tsx | 17 ++++++---- 8 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 backend/src/lib/delay/index.ts diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts index ddf57bfd5..577e3c871 100644 --- a/backend/e2e-test/mocks/keystore.ts +++ b/backend/e2e-test/mocks/keystore.ts @@ -1,7 +1,9 @@ -import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { Lock } from "@app/lib/red-lock"; import RE2 from "re2"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { delay as delayMs } from "@app/lib/delay"; +import { Lock } from "@app/lib/red-lock"; + export const mockKeyStore = (): TKeyStoreFactory => { const store: Record = {}; @@ -19,16 +21,26 @@ export const mockKeyStore = (): TKeyStoreFactory => { delete store[key]; return 1; }, - deleteItems: async (pattern) => { - const regex = new RE2(pattern.replace(/\*/g, ".*")); - let deletedCount = 0; - for (const key of Object.keys(store)) { - if (regex.test(key)) { - delete store[key]; - deletedCount += 1; + deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => { + const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`); + let totalDeleted = 0; + const keys = Object.keys(store); + + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); + + for (const key of batch) { + if (regex.test(key)) { + delete store[key]; + totalDeleted += 1; + } } + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); } - return deletedCount; + + return totalDeleted; }, getItem: async (key) => { const value = store[key]; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 9371cebb8..0e1c3e35e 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,6 +1,7 @@ import { Redis } from "ioredis"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; +import { delay as delayMs } from "@app/lib/delay"; import { Redlock, Settings } from "@app/lib/red-lock"; export const PgSqlLock = { @@ -48,6 +49,13 @@ export const KeyStoreTtls = { AccessTokenStatusUpdateInSeconds: 120 }; +type TDeleteItems = { + pattern: string; + batchSize?: number; + delay?: number; + jitter?: number; +}; + type TWaitTillReady = { key: string; waitingCb?: () => void; @@ -57,8 +65,6 @@ type TWaitTillReady = { jitter?: number; }; -const DELETION_BATCH_SIZE = 500; - export const keyStoreFactory = (redisUrl: string) => { const redis = new Redis(redisUrl); const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 }); @@ -77,7 +83,7 @@ export const keyStoreFactory = (redisUrl: string) => { const deleteItem = async (key: string) => redis.del(key); - const deleteItems = async (pattern: string) => { + const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => { let cursor = "0"; let totalDeleted = 0; @@ -87,8 +93,8 @@ export const keyStoreFactory = (redisUrl: string) => { const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads cursor = nextCursor; - for (let i = 0; i < keys.length; i += DELETION_BATCH_SIZE) { - const batch = keys.slice(i, i + DELETION_BATCH_SIZE); + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); const pipeline = redis.pipeline(); for (const key of batch) { pipeline.unlink(key); @@ -96,6 +102,9 @@ export const keyStoreFactory = (redisUrl: string) => { // eslint-disable-next-line no-await-in-loop await pipeline.exec(); totalDeleted += batch.length; + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); } } while (cursor !== "0"); diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts index 55699207f..eab3a32dd 100644 --- a/backend/src/keystore/memory.ts +++ b/backend/src/keystore/memory.ts @@ -1,5 +1,6 @@ import RE2 from "re2"; +import { delay as delayMs } from "@app/lib/delay"; import { Lock } from "@app/lib/red-lock"; import { TKeyStoreFactory } from "./keystore"; @@ -21,16 +22,26 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => { delete store[key]; return 1; }, - deleteItems: async (pattern) => { - const regex = new RE2(pattern.replace(/\*/g, ".*")); - let deletedCount = 0; - for (const key of Object.keys(store)) { - if (regex.test(key)) { - delete store[key]; - deletedCount += 1; + deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => { + const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`); + let totalDeleted = 0; + const keys = Object.keys(store); + + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); + + for (const key of batch) { + if (regex.test(key)) { + delete store[key]; + totalDeleted += 1; + } } + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); } - return deletedCount; + + return totalDeleted; }, getItem: async (key) => { const value = store[key]; diff --git a/backend/src/lib/delay/index.ts b/backend/src/lib/delay/index.ts new file mode 100644 index 000000000..32cb8ebfc --- /dev/null +++ b/backend/src/lib/delay/index.ts @@ -0,0 +1,4 @@ +export const delay = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index c1dbe3805..b436e2e18 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -553,19 +553,25 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }) } }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, handler: async (req) => { - await server.services.superAdmin.invalidateCache(req.body.type); + const keysCleared = await server.services.superAdmin.invalidateCache(req.body.type); await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.InvalidateCache, distinctId: getTelemetryDistinctId(req), properties: { + keysCleared, ...req.auditLogInfo } }); return { - message: "Successfully purged cache" + message: `Successfully invalidated ${keysCleared} cached items` }; } }); diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index f993d2398..e1136cf0e 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -572,7 +572,12 @@ export const superAdminServiceFactory = ({ }; const invalidateCache = async (type: CacheType) => { - if (type === CacheType.ALL || type === CacheType.SECRETS) await keyStore.deleteItems("secret-manager:*"); + let totalKeysCleared = 0; + + if (type === CacheType.ALL || type === CacheType.SECRETS) + totalKeysCleared += await keyStore.deleteItems({ pattern: "secret-manager:*" }); + + return totalKeysCleared; }; return { diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index 9e046cdbd..74dd7448c 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -207,6 +207,7 @@ export type TIssueCertificateEvent = { export type TInvalidateCacheEvent = { event: PostHogEventTypes.InvalidateCache; properties: { + keysCleared: number; userAgent?: string; }; }; diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index a30042999..338faaa4e 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -11,7 +11,7 @@ export const CachingPanel = () => { const { mutateAsync: invalidateCache } = useInvalidateCache(); const { membership } = useOrgPermission(); - const [type, setType] = useState(CacheType.ALL); + const [type, setType] = useState(null); const [isLoading, setIsLoading] = useState(false); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -19,25 +19,28 @@ export const CachingPanel = () => { ] as const); const handleInvalidateCacheSubmit = async () => { - try { - setIsLoading(true); + if (!type) return; + setIsLoading(true); + try { await invalidateCache({ type }); createNotification({ - text: `Successfully purged ${type} cache`, + text: `Successfully invalidated ${type} cache`, type: "success" }); - setIsLoading(false); + setType(null); handlePopUpClose("invalidateCache"); } catch (err) { console.error(err); createNotification({ - text: `Failed to purge ${type} cache`, + text: `Failed to invalidate ${type} cache`, type: "error" }); } + + setIsLoading(false); }; return ( @@ -90,7 +93,7 @@ export const CachingPanel = () => { handlePopUpToggle("invalidateCache", isOpen)} deleteKey="confirm" onDeleteApproved={handleInvalidateCacheSubmit} From a6f280197b5611a0144e5f487829bea54e4f6d5a Mon Sep 17 00:00:00 2001 From: x Date: Thu, 1 May 2025 17:37:54 -0400 Subject: [PATCH 03/16] spelling fix --- .../src/pages/admin/OverviewPage/components/CachingPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index 338faaa4e..d61e6995a 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -92,7 +92,7 @@ export const CachingPanel = () => { handlePopUpToggle("invalidateCache", isOpen)} deleteKey="confirm" From 9849a5f136e34288bcfb8b8083603192a8a24278 Mon Sep 17 00:00:00 2001 From: x Date: Fri, 2 May 2025 13:00:37 -0400 Subject: [PATCH 04/16] switched to applyJitter functions --- backend/e2e-test/mocks/keystore.ts | 3 ++- backend/src/keystore/keystore.ts | 5 +++-- backend/src/keystore/memory.ts | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts index 577e3c871..f4f251616 100644 --- a/backend/e2e-test/mocks/keystore.ts +++ b/backend/e2e-test/mocks/keystore.ts @@ -1,6 +1,7 @@ import RE2 from "re2"; import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { applyJitter } from "@app/lib/dates"; import { delay as delayMs } from "@app/lib/delay"; import { Lock } from "@app/lib/red-lock"; @@ -37,7 +38,7 @@ export const mockKeyStore = (): TKeyStoreFactory => { } // eslint-disable-next-line no-await-in-loop - await delayMs(Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); + await delayMs(Math.max(0, applyJitter(delay, jitter))); } return totalDeleted; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 0e1c3e35e..5f5eb3f50 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,6 +1,7 @@ import { Redis } from "ioredis"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; +import { applyJitter } from "@app/lib/dates"; import { delay as delayMs } from "@app/lib/delay"; import { Redlock, Settings } from "@app/lib/red-lock"; @@ -104,7 +105,7 @@ export const keyStoreFactory = (redisUrl: string) => { totalDeleted += batch.length; // eslint-disable-next-line no-await-in-loop - await delayMs(Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); + await delayMs(Math.max(0, applyJitter(delay, jitter))); } } while (cursor !== "0"); @@ -130,7 +131,7 @@ export const keyStoreFactory = (redisUrl: string) => { // eslint-disable-next-line await new Promise((resolve) => { waitingCb?.(); - setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); + setTimeout(resolve, Math.max(0, applyJitter(delay, jitter))); }); attempts += 1; // eslint-disable-next-line diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts index eab3a32dd..84cd06c03 100644 --- a/backend/src/keystore/memory.ts +++ b/backend/src/keystore/memory.ts @@ -1,5 +1,6 @@ import RE2 from "re2"; +import { applyJitter } from "@app/lib/dates"; import { delay as delayMs } from "@app/lib/delay"; import { Lock } from "@app/lib/red-lock"; @@ -38,7 +39,7 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => { } // eslint-disable-next-line no-await-in-loop - await delayMs(Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); + await delayMs(Math.max(0, applyJitter(delay, jitter))); } return totalDeleted; From d13e685a81039e1b26ace0b699be17b2654e43d7 Mon Sep 17 00:00:00 2001 From: x Date: Fri, 2 May 2025 13:04:22 -0400 Subject: [PATCH 05/16] emphasize that secrets cache is encrypted in frontend --- .../src/pages/admin/OverviewPage/components/CachingPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index d61e6995a..0d5e1c2ba 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -49,8 +49,8 @@ export const CachingPanel = () => {
Secrets Cache - The secrets cache encompasses all secrets stored within the system and provides a - temporary, secure storage location for frequently accessed credentials. + The encrypted secrets cache encompasses all secrets stored within the system and + provides a temporary, secure storage location for frequently accessed credentials.
From 877485b45aa5883448b314fce85a9f23a20de38b Mon Sep 17 00:00:00 2001 From: x Date: Fri, 2 May 2025 15:23:35 -0400 Subject: [PATCH 06/16] queue job --- backend/src/@types/fastify.d.ts | 2 +- backend/src/queue/queue-service.ts | 15 ++++++- backend/src/server/routes/index.ts | 9 +++- backend/src/server/routes/v1/admin-router.ts | 5 +-- backend/src/server/routes/v1/index.ts | 2 +- .../super-admin/invalidate-cache-queue.ts | 43 +++++++++++++++++++ .../super-admin/super-admin-service.ts | 14 +++--- .../src/services/telemetry/telemetry-types.ts | 1 - 8 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 backend/src/services/super-admin/invalidate-cache-queue.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 92c22874e..77db3e59e 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -71,6 +71,7 @@ import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-a import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; +import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; @@ -100,7 +101,6 @@ import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; -import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; declare module "@fastify/request-context" { interface RequestContextData { diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index ae1a3e821..807d6c286 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -25,6 +25,7 @@ import { TQueueSecretSyncSyncSecretsByIdDTO, TQueueSendSecretSyncActionFailedNotificationsDTO } from "@app/services/secret-sync/secret-sync-types"; +import { CacheType } from "@app/services/super-admin/super-admin-types"; import { TWebhookPayloads } from "@app/services/webhook/webhook-types"; export enum QueueName { @@ -49,7 +50,8 @@ export enum QueueName { AccessTokenStatusUpdate = "access-token-status-update", ImportSecretsFromExternalSource = "import-secrets-from-external-source", AppConnectionSecretSync = "app-connection-secret-sync", - SecretRotationV2 = "secret-rotation-v2" + SecretRotationV2 = "secret-rotation-v2", + InvalidateCache = "invalidate-cache" } export enum QueueJobs { @@ -81,7 +83,8 @@ export enum QueueJobs { SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications", SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations", SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", - SecretRotationV2SendNotification = "secret-rotation-v2-send-notification" + SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", + InvalidateCache = "invalidate-cache" } export type TQueueJobTypes = { @@ -234,6 +237,14 @@ export type TQueueJobTypes = { name: QueueJobs.SecretRotationV2SendNotification; payload: TSecretRotationSendNotificationJobPayload; }; + [QueueName.InvalidateCache]: { + name: QueueJobs.InvalidateCache; + payload: { + data: { + type: CacheType; + }; + }; + }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 7e999cfb8..fb9c2fc32 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -238,6 +238,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack- import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal"; import { slackServiceFactory } from "@app/services/slack/slack-service"; import { TSmtpService } from "@app/services/smtp/smtp-service"; +import { invalidateCacheQueueFactory } from "@app/services/super-admin/invalidate-cache-queue"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal"; @@ -605,6 +606,11 @@ export const registerRoutes = async ( queueService }); + const invalidateCacheQueue = invalidateCacheQueueFactory({ + keyStore, + queueService + }); + const userService = userServiceFactory({ userDAL, userAliasDAL, @@ -715,7 +721,8 @@ export const registerRoutes = async ( keyStore, licenseService, kmsService, - microsoftTeamsService + microsoftTeamsService, + invalidateCacheQueue }); const orgAdminService = orgAdminServiceFactory({ diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 322419609..13d4e2728 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -572,19 +572,18 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const keysCleared = await server.services.superAdmin.invalidateCache(req.body.type); + await server.services.superAdmin.invalidateCache(req.body.type); await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.InvalidateCache, distinctId: getTelemetryDistinctId(req), properties: { - keysCleared, ...req.auditLogInfo } }); return { - message: `Successfully invalidated ${keysCleared} cached items` + message: "Cache invalidation job started" }; } }); diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index d2ba35a7e..a50299555 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -26,6 +26,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationRouter } from "./integration-router"; import { registerInviteOrgRouter } from "./invite-org-router"; +import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router"; import { registerOrgAdminRouter } from "./org-admin-router"; import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; @@ -47,7 +48,6 @@ import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; -import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router"; export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerSsoRouter, { prefix: "/sso" }); diff --git a/backend/src/services/super-admin/invalidate-cache-queue.ts b/backend/src/services/super-admin/invalidate-cache-queue.ts new file mode 100644 index 000000000..2bc897df6 --- /dev/null +++ b/backend/src/services/super-admin/invalidate-cache-queue.ts @@ -0,0 +1,43 @@ +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { CacheType } from "./super-admin-types"; + +export type TInvalidateCacheQueueFactoryDep = { + queueService: TQueueServiceFactory; + + keyStore: Pick; +}; + +export type TInvalidateCacheQueueFactory = ReturnType; + +export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalidateCacheQueueFactoryDep) => { + const startInvalidate = async (dto: { + data: { + type: CacheType; + }; + }) => { + await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, { + removeOnComplete: true, + removeOnFail: true + }); + }; + + queueService.start(QueueName.InvalidateCache, async (job) => { + try { + const { + data: { type } + } = job.data; + + if (type === CacheType.ALL || type === CacheType.SECRETS) + await keyStore.deleteItems({ pattern: "secret-manager:*" }); + } catch (err) { + logger.error(err, "Failed to invalidate cache"); + } + }); + + return { + startInvalidate + }; +}; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index c35b68d52..2e169f064 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -25,6 +25,7 @@ import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; +import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { CacheType, @@ -50,6 +51,7 @@ type TSuperAdminServiceFactoryDep = { keyStore: Pick; licenseService: Pick; microsoftTeamsService: Pick; + invalidateCacheQueue: TInvalidateCacheQueueFactory; }; export type TSuperAdminServiceFactory = ReturnType; @@ -81,7 +83,8 @@ export const superAdminServiceFactory = ({ identityAccessTokenDAL, identityTokenAuthDAL, identityOrgMembershipDAL, - microsoftTeamsService + microsoftTeamsService, + invalidateCacheQueue }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -633,12 +636,9 @@ export const superAdminServiceFactory = ({ }; const invalidateCache = async (type: CacheType) => { - let totalKeysCleared = 0; - - if (type === CacheType.ALL || type === CacheType.SECRETS) - totalKeysCleared += await keyStore.deleteItems({ pattern: "secret-manager:*" }); - - return totalKeysCleared; + await invalidateCacheQueue.startInvalidate({ + data: { type } + }); }; return { diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index 74dd7448c..9e046cdbd 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -207,7 +207,6 @@ export type TIssueCertificateEvent = { export type TInvalidateCacheEvent = { event: PostHogEventTypes.InvalidateCache; properties: { - keysCleared: number; userAgent?: string; }; }; From 85c1a1081e08a044325e2805bd38afe04141ae43 Mon Sep 17 00:00:00 2001 From: x Date: Fri, 2 May 2025 18:43:07 -0400 Subject: [PATCH 07/16] checkpoint --- ...1443_invalidate-cache-status-superadmin.ts | 21 ++++ backend/src/db/schemas/organizations.ts | 1 - backend/src/db/schemas/projects.ts | 2 +- backend/src/db/schemas/super-admin.ts | 3 +- backend/src/keystore/keystore.ts | 1 + backend/src/server/routes/v1/admin-router.ts | 27 ++++++ .../super-admin/invalidate-cache-queue.ts | 23 ++++- .../super-admin/super-admin-service.ts | 9 +- frontend/src/hooks/api/admin/queries.ts | 14 +++ frontend/src/hooks/api/admin/types.ts | 5 + .../OverviewPage/components/CachingPanel.tsx | 97 ++++++++++++++++--- frontend/vite.config.ts | 64 ++++++------ 12 files changed, 220 insertions(+), 47 deletions(-) create mode 100644 backend/src/db/migrations/20250502201443_invalidate-cache-status-superadmin.ts diff --git a/backend/src/db/migrations/20250502201443_invalidate-cache-status-superadmin.ts b/backend/src/db/migrations/20250502201443_invalidate-cache-status-superadmin.ts new file mode 100644 index 000000000..a3a5805c1 --- /dev/null +++ b/backend/src/db/migrations/20250502201443_invalidate-cache-status-superadmin.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "invalidatingCache"); + if (!hasColumn) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.boolean("invalidatingCache").notNullable().defaultTo(false); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "invalidatingCache"); + if (hasColumn) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("invalidatingCache"); + }); + } +} diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index bc6f0b7af..8d8279802 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -23,7 +23,6 @@ export const OrganizationsSchema = z.object({ defaultMembershipRole: z.string().default("member"), enforceMfa: z.boolean().default(false), selectedMfaMethod: z.string().nullable().optional(), - secretShareSendToAnyone: z.boolean().default(true).nullable().optional(), allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(), shouldUseNewPrivilegeSystem: z.boolean().default(true), privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 2403d6cf4..297601fd0 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -27,7 +27,7 @@ export const ProjectsSchema = z.object({ description: z.string().nullable().optional(), type: z.string(), enforceCapitalization: z.boolean().default(false), - hasDeleteProtection: z.boolean().default(true).nullable().optional() + hasDeleteProtection: z.boolean().default(false).nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index ec35042ad..18e45a5f0 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -29,7 +29,8 @@ export const SuperAdminSchema = z.object({ adminIdentityIds: z.string().array().nullable().optional(), encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(), - encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional() + encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional(), + invalidatingCache: z.boolean().default(false) }); export type TSuperAdmin = z.infer; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 5f5eb3f50..6da6c4fa4 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -103,6 +103,7 @@ export const keyStoreFactory = (redisUrl: string) => { // eslint-disable-next-line no-await-in-loop await pipeline.exec(); totalDeleted += batch.length; + console.log("BATCH DONE"); // eslint-disable-next-line no-await-in-loop await delayMs(Math.max(0, applyJitter(delay, jitter))); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 13d4e2728..8610a611b 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -587,4 +587,31 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "GET", + url: "/invalidating-cache-status", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + invalidating: z.boolean() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + const invalidating = await server.services.superAdmin.checkIfInvalidatingCache(); + + return { + invalidating + }; + } + }); }; diff --git a/backend/src/services/super-admin/invalidate-cache-queue.ts b/backend/src/services/super-admin/invalidate-cache-queue.ts index 2bc897df6..d93df0052 100644 --- a/backend/src/services/super-admin/invalidate-cache-queue.ts +++ b/backend/src/services/super-admin/invalidate-cache-queue.ts @@ -1,4 +1,5 @@ import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { delay } from "@app/lib/delay"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; @@ -7,7 +8,7 @@ import { CacheType } from "./super-admin-types"; export type TInvalidateCacheQueueFactoryDep = { queueService: TQueueServiceFactory; - keyStore: Pick; + keyStore: Pick; }; export type TInvalidateCacheQueueFactory = ReturnType; @@ -18,9 +19,18 @@ export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalid type: CacheType; }; }) => { + // Cancel existing jobs if any + try { + console.log("stopping job"); + await queueService.clearQueue(QueueName.InvalidateCache); + } catch (err) { + logger.warn(err, "Failed to clear queue"); + } + await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, { removeOnComplete: true, - removeOnFail: true + removeOnFail: true, + jobId: "invalidate-cache" }); }; @@ -30,10 +40,19 @@ export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalid data: { type } } = job.data; + await keyStore.setItemWithExpiry("invalidating-cache", 3600, "true"); // 1 hour max (in case the job somehow silently fails) + + console.log("STARTING JOB"); + if (type === CacheType.ALL || type === CacheType.SECRETS) await keyStore.deleteItems({ pattern: "secret-manager:*" }); + + // await delay(12000); // TODO(andrey): Remove. It's for debug + + await keyStore.deleteItem("invalidating-cache"); } catch (err) { logger.error(err, "Failed to invalidate cache"); + await keyStore.deleteItem("invalidating-cache"); } }); diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 2e169f064..b21b97911 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -67,7 +67,7 @@ export let getServerCfg: () => Promise< const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY_EXP = 60; // 60s -const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; +export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, @@ -641,6 +641,10 @@ export const superAdminServiceFactory = ({ }); }; + const checkIfInvalidatingCache = async () => { + return (await keyStore.getItem("invalidating-cache")) !== null; + }; + return { initServerCfg, updateServerCfg, @@ -655,6 +659,7 @@ export const superAdminServiceFactory = ({ grantServerAdminAccessToUser, deleteIdentitySuperAdminAccess, deleteUserSuperAdminAccess, - invalidateCache + invalidateCache, + checkIfInvalidatingCache }; }; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 75e826bce..b9bdf223a 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -8,6 +8,7 @@ import { AdminGetIdentitiesFilters, AdminGetUsersFilters, AdminIntegrationsConfig, + TGetInvalidatingCacheStatus, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; @@ -120,3 +121,16 @@ export const useGetServerRootKmsEncryptionDetails = () => { } }); }; + +export const useGetInvalidatingCacheStatus = () => { + return useQuery({ + queryKey: adminQueryKeys.getInvalidateCache(), + queryFn: async () => { + const { data } = await apiRequest.get( + "/api/v1/admin/invalidating-cache-status" + ); + + return data.invalidating; + } + }); +}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 7e808a101..8850b3375 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -24,6 +24,7 @@ export type TServerConfig = { enabledLoginMethods: LoginMethod[]; authConsentContent?: string; pageFrameContent?: string; + invalidatingCache: boolean; }; export type TUpdateServerConfigDTO = { @@ -93,3 +94,7 @@ export enum CacheType { export type TInvalidateCacheDTO = { type: CacheType; }; + +export type TGetInvalidatingCacheStatus = { + invalidating: boolean; +}; diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index 0d5e1c2ba..f8b8f43b8 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -1,37 +1,66 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { createNotification } from "@app/components/notifications"; -import { Button, DeleteActionModal } from "@app/components/v2"; +import { Badge, Button, DeleteActionModal } from "@app/components/v2"; import { useOrgPermission } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useInvalidateCache } from "@app/hooks/api"; import { CacheType } from "@app/hooks/api/admin/types"; +import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faRotate } from "@fortawesome/free-solid-svg-icons"; export const CachingPanel = () => { const { mutateAsync: invalidateCache } = useInvalidateCache(); + const { data: isInvalidating, refetch: refetchInvalidatingStatus } = + useGetInvalidatingCacheStatus(); const { membership } = useOrgPermission(); + const wasInvalidating = useRef(false); const [type, setType] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [buttonsDisabled, setButtonsDisabled] = useState(false); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "invalidateCache" ] as const); + const success = () => { + createNotification({ + text: `Successfully invalidated cache`, + type: "success" + }); + setButtonsDisabled(false); + }; + + const timeoutRef = useRef(null); + const disableButtons = () => { + // Enable buttons after 10 seconds, even if still invalidating + setButtonsDisabled(true); + timeoutRef.current = setTimeout(() => { + setButtonsDisabled(false); + }, 10000); + }; + const handleInvalidateCacheSubmit = async () => { if (!type) return; - setIsLoading(true); try { await invalidateCache({ type }); + wasInvalidating.current = true; + createNotification({ - text: `Successfully invalidated ${type} cache`, + text: `Began invalidating ${type} cache`, type: "success" }); - setType(null); + disableButtons(); handlePopUpClose("invalidateCache"); + + if (!(await refetchInvalidatingStatus()).data) { + success(); + return; + } } catch (err) { console.error(err); createNotification({ @@ -40,14 +69,61 @@ export const CachingPanel = () => { }); } - setIsLoading(false); + setType(null); }; + const pollingRef = useRef(null); + + // Update the "invalidating cache" status + useEffect(() => { + if (!isInvalidating) return; + + if (pollingRef.current) clearInterval(pollingRef.current); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + + // Start polling every 3 seconds + pollingRef.current = setInterval(async () => { + try { + await refetchInvalidatingStatus(); + } catch (err) { + console.error("Polling error:", err); + } + }, 3000); + + disableButtons(); + + return () => { + if (pollingRef.current) clearInterval(pollingRef.current); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, [isInvalidating]); + + useEffect(() => { + if (isInvalidating === false && wasInvalidating.current) { + success(); + wasInvalidating.current = false; + + if (pollingRef.current) clearInterval(pollingRef.current); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + } + }, [isInvalidating]); + return ( <>
- Secrets Cache +
+ Secrets Cache + {isInvalidating && ( + + + Invalidating Cache + + )} +
The encrypted secrets cache encompasses all secrets stored within the system and provides a temporary, secure storage location for frequently accessed credentials. @@ -56,12 +132,11 @@ export const CachingPanel = () => { @@ -93,7 +168,7 @@ export const CachingPanel = () => { handlePopUpToggle("invalidateCache", isOpen)} deleteKey="confirm" onDeleteApproved={handleInvalidateCacheSubmit} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b73752773..618dcb618 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,6 +1,6 @@ import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react-swc"; -import { defineConfig, PluginOption } from "vite"; +import { defineConfig, loadEnv, PluginOption } from "vite"; import { nodePolyfills } from "vite-plugin-node-polyfills"; import topLevelAwait from "vite-plugin-top-level-await"; import wasm from "vite-plugin-wasm"; @@ -20,32 +20,38 @@ const virtualRouteFileChangeReloadPlugin: PluginOption = { }; // https://vite.dev/config/ -export default defineConfig({ - server: { - host: true, - port: 3000 - // proxy: { - // "/api": { - // target: "http://localhost:8080", - // changeOrigin: true, - // secure: false, - // ws: true - // } - // } - }, - plugins: [ - tsconfigPaths(), - nodePolyfills({ - globals: { - Buffer: true - } - }), - wasm(), - topLevelAwait(), - TanStackRouterVite({ - virtualRouteConfig: "./src/routes.ts" - }), - react(), - virtualRouteFileChangeReloadPlugin - ] +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + const allowedHosts = env.VITE_ALLOWED_HOSTS?.split(",") ?? []; + + return { + server: { + allowedHosts, + host: true, + port: 3000 + // proxy: { + // "/api": { + // target: "http://localhost:8080", + // changeOrigin: true, + // secure: false, + // ws: true + // } + // } + }, + plugins: [ + tsconfigPaths(), + nodePolyfills({ + globals: { + Buffer: true + } + }), + wasm(), + topLevelAwait(), + TanStackRouterVite({ + virtualRouteConfig: "./src/routes.ts" + }), + react(), + virtualRouteFileChangeReloadPlugin + ] + }; }); From 6eea4c8364c04b3e8810d2749e34bdbf98b75f21 Mon Sep 17 00:00:00 2001 From: x Date: Fri, 2 May 2025 19:20:02 -0400 Subject: [PATCH 08/16] frontend tweaks --- .../super-admin/invalidate-cache-queue.ts | 15 +-------------- .../OverviewPage/components/CachingPanel.tsx | 16 +++++++++++----- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/backend/src/services/super-admin/invalidate-cache-queue.ts b/backend/src/services/super-admin/invalidate-cache-queue.ts index d93df0052..0ac195343 100644 --- a/backend/src/services/super-admin/invalidate-cache-queue.ts +++ b/backend/src/services/super-admin/invalidate-cache-queue.ts @@ -1,5 +1,4 @@ import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { delay } from "@app/lib/delay"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; @@ -19,14 +18,6 @@ export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalid type: CacheType; }; }) => { - // Cancel existing jobs if any - try { - console.log("stopping job"); - await queueService.clearQueue(QueueName.InvalidateCache); - } catch (err) { - logger.warn(err, "Failed to clear queue"); - } - await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, { removeOnComplete: true, removeOnFail: true, @@ -40,15 +31,11 @@ export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalid data: { type } } = job.data; - await keyStore.setItemWithExpiry("invalidating-cache", 3600, "true"); // 1 hour max (in case the job somehow silently fails) - - console.log("STARTING JOB"); + await keyStore.setItemWithExpiry("invalidating-cache", 1800, "true"); // 30 minutes max (in case the job somehow silently fails) if (type === CacheType.ALL || type === CacheType.SECRETS) await keyStore.deleteItems({ pattern: "secret-manager:*" }); - // await delay(12000); // TODO(andrey): Remove. It's for debug - await keyStore.deleteItem("invalidating-cache"); } catch (err) { logger.error(err, "Failed to invalidate cache"); diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index f8b8f43b8..49d6d9107 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -16,7 +16,7 @@ export const CachingPanel = () => { useGetInvalidatingCacheStatus(); const { membership } = useOrgPermission(); - const wasInvalidating = useRef(false); + const ignoreInitial = useRef(true); const [type, setType] = useState(null); const [buttonsDisabled, setButtonsDisabled] = useState(false); @@ -47,8 +47,6 @@ export const CachingPanel = () => { try { await invalidateCache({ type }); - wasInvalidating.current = true; - createNotification({ text: `Began invalidating ${type} cache`, type: "success" @@ -98,10 +96,18 @@ export const CachingPanel = () => { }; }, [isInvalidating]); + // Helper to ignore the initial useEffect calls for isInvalidating useEffect(() => { - if (isInvalidating === false && wasInvalidating.current) { + const timer = setTimeout(() => { + ignoreInitial.current = false; + }, 1000); + + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + if (!ignoreInitial.current && isInvalidating === false) { success(); - wasInvalidating.current = false; if (pollingRef.current) clearInterval(pollingRef.current); if (timeoutRef.current) clearTimeout(timeoutRef.current); From f49fb534abf91b68fa087e7a426e041127cd4f49 Mon Sep 17 00:00:00 2001 From: x Date: Fri, 2 May 2025 19:50:55 -0400 Subject: [PATCH 09/16] review fixes --- ...1443_invalidate-cache-status-superadmin.ts | 21 --- backend/src/db/schemas/super-admin.ts | 3 +- .../secret-rotation-v2-fns.ts | 2 +- .../ssh/ssh-certificate-authority-service.ts | 4 +- .../src/services/auth/auth-login-service.ts | 4 +- .../services/secret-sync/secret-sync-fns.ts | 2 +- .../super-admin/invalidate-cache-queue.ts | 2 +- .../super-admin/super-admin-service.ts | 4 +- .../OverviewPage/components/CachingPanel.tsx | 125 ++++++------------ 9 files changed, 50 insertions(+), 117 deletions(-) delete mode 100644 backend/src/db/migrations/20250502201443_invalidate-cache-status-superadmin.ts diff --git a/backend/src/db/migrations/20250502201443_invalidate-cache-status-superadmin.ts b/backend/src/db/migrations/20250502201443_invalidate-cache-status-superadmin.ts deleted file mode 100644 index a3a5805c1..000000000 --- a/backend/src/db/migrations/20250502201443_invalidate-cache-status-superadmin.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Knex } from "knex"; - -import { TableName } from "../schemas"; - -export async function up(knex: Knex): Promise { - const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "invalidatingCache"); - if (!hasColumn) { - await knex.schema.alterTable(TableName.SuperAdmin, (t) => { - t.boolean("invalidatingCache").notNullable().defaultTo(false); - }); - } -} - -export async function down(knex: Knex): Promise { - const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "invalidatingCache"); - if (hasColumn) { - await knex.schema.alterTable(TableName.SuperAdmin, (t) => { - t.dropColumn("invalidatingCache"); - }); - } -} diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 18e45a5f0..ec35042ad 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -29,8 +29,7 @@ export const SuperAdminSchema = z.object({ adminIdentityIds: z.string().array().nullable().optional(), encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(), - encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional(), - invalidatingCache: z.boolean().default(false) + encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 5c0d97ee8..a25482c8c 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -219,7 +219,7 @@ export const parseRotationErrorMessage = (err: unknown): string => { if (err instanceof AxiosError) { errorMessage += err?.response?.data ? JSON.stringify(err?.response?.data) - : err?.message ?? "An unknown error occurred."; + : (err?.message ?? "An unknown error occurred."); } else { errorMessage += (err as Error)?.message || "An unknown error occurred."; } diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts index 312b7966b..d58644d90 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -282,7 +282,7 @@ export const sshCertificateAuthorityServiceFactory = ({ // set [keyId] depending on if [allowCustomKeyIds] is true or false const keyId = sshCertificateTemplate.allowCustomKeyIds - ? requestedKeyId ?? `${actor}-${actorId}` + ? (requestedKeyId ?? `${actor}-${actorId}`) : `${actor}-${actorId}`; const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId }); @@ -404,7 +404,7 @@ export const sshCertificateAuthorityServiceFactory = ({ // set [keyId] depending on if [allowCustomKeyIds] is true or false const keyId = sshCertificateTemplate.allowCustomKeyIds - ? requestedKeyId ?? `${actor}-${actorId}` + ? (requestedKeyId ?? `${actor}-${actorId}`) : `${actor}-${actorId}`; const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId }); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 14aa8f038..2e3f19a2c 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -401,8 +401,8 @@ export const authLoginServiceFactory = ({ } const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled; - const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL : undefined; - const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod ?? MfaMethod.EMAIL : undefined; + const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; + const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; const mfaMethod = orgMfaMethod ?? userMfaMethod; if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index f5737edb3..5749852d7 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -291,7 +291,7 @@ export const parseSyncErrorMessage = (err: unknown): string => { } else if (err instanceof AxiosError) { errorMessage = err?.response?.data ? JSON.stringify(err?.response?.data) - : err?.message ?? "An unknown error occurred."; + : (err?.message ?? "An unknown error occurred."); } else { errorMessage = (err as Error)?.message || "An unknown error occurred."; } diff --git a/backend/src/services/super-admin/invalidate-cache-queue.ts b/backend/src/services/super-admin/invalidate-cache-queue.ts index 0ac195343..c2a12f5d5 100644 --- a/backend/src/services/super-admin/invalidate-cache-queue.ts +++ b/backend/src/services/super-admin/invalidate-cache-queue.ts @@ -21,7 +21,7 @@ export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalid await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, { removeOnComplete: true, removeOnFail: true, - jobId: "invalidate-cache" + jobId: `invalidate-cache-${dto.data.type}` }); }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b21b97911..7c9ca4f38 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -176,8 +176,8 @@ export const superAdminServiceFactory = ({ const canServerAdminAccessAfterApply = data.enabledLoginMethods.some((loginMethod) => - loginMethodToAuthMethod[loginMethod as LoginMethod].some( - (authMethod) => superAdminUser.authMethods?.includes(authMethod) + loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) => + superAdminUser.authMethods?.includes(authMethod) ) ) || isUserSamlAccessEnabled || diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index 49d6d9107..f46aa4cca 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -1,14 +1,15 @@ +/* eslint-disable no-return-assign, consistent-return */ import { useEffect, useRef, useState } from "react"; +import { faRotate } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { Badge, Button, DeleteActionModal } from "@app/components/v2"; import { useOrgPermission } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useInvalidateCache } from "@app/hooks/api"; -import { CacheType } from "@app/hooks/api/admin/types"; import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faRotate } from "@fortawesome/free-solid-svg-icons"; +import { CacheType } from "@app/hooks/api/admin/types"; export const CachingPanel = () => { const { mutateAsync: invalidateCache } = useInvalidateCache(); @@ -17,6 +18,9 @@ export const CachingPanel = () => { const { membership } = useOrgPermission(); const ignoreInitial = useRef(true); + const timeoutRef = useRef(null); + const pollingRef = useRef(null); + const [type, setType] = useState(null); const [buttonsDisabled, setButtonsDisabled] = useState(false); @@ -24,21 +28,14 @@ export const CachingPanel = () => { "invalidateCache" ] as const); - const success = () => { - createNotification({ - text: `Successfully invalidated cache`, - type: "success" - }); - setButtonsDisabled(false); + const disableButtonsTemporarily = () => { + setButtonsDisabled(true); + timeoutRef.current = setTimeout(() => setButtonsDisabled(false), 10000); }; - const timeoutRef = useRef(null); - const disableButtons = () => { - // Enable buttons after 10 seconds, even if still invalidating - setButtonsDisabled(true); - timeoutRef.current = setTimeout(() => { - setButtonsDisabled(false); - }, 10000); + const success = () => { + createNotification({ text: "Successfully invalidated cache", type: "success" }); + setButtonsDisabled(false); }; const handleInvalidateCacheSubmit = async () => { @@ -46,13 +43,8 @@ export const CachingPanel = () => { try { await invalidateCache({ type }); - - createNotification({ - text: `Began invalidating ${type} cache`, - type: "success" - }); - - disableButtons(); + createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); + disableButtonsTemporarily(); handlePopUpClose("invalidateCache"); if (!(await refetchInvalidatingStatus()).data) { @@ -61,59 +53,45 @@ export const CachingPanel = () => { } } catch (err) { console.error(err); - createNotification({ - text: `Failed to invalidate ${type} cache`, - type: "error" - }); + createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" }); } setType(null); }; - const pollingRef = useRef(null); - - // Update the "invalidating cache" status useEffect(() => { - if (!isInvalidating) return; - - if (pollingRef.current) clearInterval(pollingRef.current); - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - // Start polling every 3 seconds - pollingRef.current = setInterval(async () => { - try { - await refetchInvalidatingStatus(); - } catch (err) { - console.error("Polling error:", err); - } - }, 3000); - - disableButtons(); - - return () => { - if (pollingRef.current) clearInterval(pollingRef.current); - if (timeoutRef.current) clearTimeout(timeoutRef.current); - }; - }, [isInvalidating]); - - // Helper to ignore the initial useEffect calls for isInvalidating - useEffect(() => { - const timer = setTimeout(() => { - ignoreInitial.current = false; - }, 1000); - + const timer = setTimeout(() => (ignoreInitial.current = false), 1000); return () => clearTimeout(timer); }, []); + useEffect(() => { + if (!isInvalidating) return; + + clearInterval(pollingRef.current!); + clearTimeout(timeoutRef.current!); + + pollingRef.current = setInterval(() => { + refetchInvalidatingStatus().catch((err) => console.error("Polling error:", err)); + }, 3000); + + disableButtonsTemporarily(); + + return () => { + clearInterval(pollingRef.current!); + clearTimeout(timeoutRef.current!); + }; + }, [isInvalidating]); + useEffect(() => { if (!ignoreInitial.current && isInvalidating === false) { success(); - - if (pollingRef.current) clearInterval(pollingRef.current); - if (timeoutRef.current) clearTimeout(timeoutRef.current); + clearInterval(pollingRef.current!); + clearTimeout(timeoutRef.current!); } }, [isInvalidating]); + const isAdmin = membership?.role === "admin"; + return ( <>
@@ -142,35 +120,12 @@ export const CachingPanel = () => { setType(CacheType.SECRETS); handlePopUpOpen("invalidateCache"); }} - isDisabled={Boolean(membership && membership.role !== "admin") || buttonsDisabled} + isDisabled={!isAdmin || buttonsDisabled} > Invalidate Secrets Cache
- {/* Uncomment this when we have more than one cache type */} - {/*
-
- All Cache - - All cache refers to the entirety of cached data throughout the system, including secrets - and miscellaneous information. - -
- - -
*/} - Date: Fri, 2 May 2025 20:10:30 -0400 Subject: [PATCH 10/16] lint fixes --- .../ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts | 2 +- .../src/ee/services/ssh/ssh-certificate-authority-service.ts | 4 ++-- backend/src/services/auth/auth-login-service.ts | 4 ++-- backend/src/services/secret-sync/secret-sync-fns.ts | 2 +- backend/src/services/super-admin/super-admin-service.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index a25482c8c..5c0d97ee8 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -219,7 +219,7 @@ export const parseRotationErrorMessage = (err: unknown): string => { if (err instanceof AxiosError) { errorMessage += err?.response?.data ? JSON.stringify(err?.response?.data) - : (err?.message ?? "An unknown error occurred."); + : err?.message ?? "An unknown error occurred."; } else { errorMessage += (err as Error)?.message || "An unknown error occurred."; } diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts index d58644d90..312b7966b 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -282,7 +282,7 @@ export const sshCertificateAuthorityServiceFactory = ({ // set [keyId] depending on if [allowCustomKeyIds] is true or false const keyId = sshCertificateTemplate.allowCustomKeyIds - ? (requestedKeyId ?? `${actor}-${actorId}`) + ? requestedKeyId ?? `${actor}-${actorId}` : `${actor}-${actorId}`; const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId }); @@ -404,7 +404,7 @@ export const sshCertificateAuthorityServiceFactory = ({ // set [keyId] depending on if [allowCustomKeyIds] is true or false const keyId = sshCertificateTemplate.allowCustomKeyIds - ? (requestedKeyId ?? `${actor}-${actorId}`) + ? requestedKeyId ?? `${actor}-${actorId}` : `${actor}-${actorId}`; const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId }); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 2e3f19a2c..14aa8f038 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -401,8 +401,8 @@ export const authLoginServiceFactory = ({ } const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled; - const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; - const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; + const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL : undefined; + const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod ?? MfaMethod.EMAIL : undefined; const mfaMethod = orgMfaMethod ?? userMfaMethod; if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 5749852d7..f5737edb3 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -291,7 +291,7 @@ export const parseSyncErrorMessage = (err: unknown): string => { } else if (err instanceof AxiosError) { errorMessage = err?.response?.data ? JSON.stringify(err?.response?.data) - : (err?.message ?? "An unknown error occurred."); + : err?.message ?? "An unknown error occurred."; } else { errorMessage = (err as Error)?.message || "An unknown error occurred."; } diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 7c9ca4f38..b21b97911 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -176,8 +176,8 @@ export const superAdminServiceFactory = ({ const canServerAdminAccessAfterApply = data.enabledLoginMethods.some((loginMethod) => - loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) => - superAdminUser.authMethods?.includes(authMethod) + loginMethodToAuthMethod[loginMethod as LoginMethod].some( + (authMethod) => superAdminUser.authMethods?.includes(authMethod) ) ) || isUserSamlAccessEnabled || From dc59f226b65eee616d8900e373174ee3cd2e4efb Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 5 May 2025 15:58:45 -0400 Subject: [PATCH 11/16] swapped polling to react query --- frontend/src/hooks/api/admin/queries.ts | 6 ++-- .../OverviewPage/components/CachingPanel.tsx | 34 +++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index b9bdf223a..85c6c153e 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -122,7 +122,7 @@ export const useGetServerRootKmsEncryptionDetails = () => { }); }; -export const useGetInvalidatingCacheStatus = () => { +export const useGetInvalidatingCacheStatus = (enabled = true) => { return useQuery({ queryKey: adminQueryKeys.getInvalidateCache(), queryFn: async () => { @@ -131,6 +131,8 @@ export const useGetInvalidatingCacheStatus = () => { ); return data.invalidating; - } + }, + enabled, + refetchInterval: (data) => (data ? 3000 : false) }); }; diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index f46aa4cca..719410b29 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-return-assign, consistent-return */ import { useEffect, useRef, useState } from "react"; import { faRotate } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -13,16 +12,17 @@ import { CacheType } from "@app/hooks/api/admin/types"; export const CachingPanel = () => { const { mutateAsync: invalidateCache } = useInvalidateCache(); - const { data: isInvalidating, refetch: refetchInvalidatingStatus } = - useGetInvalidatingCacheStatus(); const { membership } = useOrgPermission(); - const ignoreInitial = useRef(true); + const hasShownSuccessRef = useRef(true); const timeoutRef = useRef(null); - const pollingRef = useRef(null); const [type, setType] = useState(null); const [buttonsDisabled, setButtonsDisabled] = useState(false); + const [shouldPoll, setShouldPoll] = useState(false); + + const { data: isInvalidating, refetch: refetchInvalidatingStatus } = + useGetInvalidatingCacheStatus(shouldPoll); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "invalidateCache" @@ -34,13 +34,19 @@ export const CachingPanel = () => { }; const success = () => { + if (hasShownSuccessRef.current) return; + hasShownSuccessRef.current = true; + createNotification({ text: "Successfully invalidated cache", type: "success" }); setButtonsDisabled(false); + setShouldPoll(false); }; const handleInvalidateCacheSubmit = async () => { if (!type) return; + hasShownSuccessRef.current = false; + try { await invalidateCache({ type }); createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); @@ -51,41 +57,35 @@ export const CachingPanel = () => { success(); return; } + + setShouldPoll(true); } catch (err) { console.error(err); createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" }); } - - setType(null); }; useEffect(() => { - const timer = setTimeout(() => (ignoreInitial.current = false), 1000); + refetchInvalidatingStatus(); + const timer = setTimeout(() => (hasShownSuccessRef.current = false), 1000); return () => clearTimeout(timer); }, []); useEffect(() => { if (!isInvalidating) return; - clearInterval(pollingRef.current!); clearTimeout(timeoutRef.current!); - - pollingRef.current = setInterval(() => { - refetchInvalidatingStatus().catch((err) => console.error("Polling error:", err)); - }, 3000); - + setShouldPoll(true); disableButtonsTemporarily(); return () => { - clearInterval(pollingRef.current!); clearTimeout(timeoutRef.current!); }; }, [isInvalidating]); useEffect(() => { - if (!ignoreInitial.current && isInvalidating === false) { + if (!hasShownSuccessRef.current && isInvalidating === false) { success(); - clearInterval(pollingRef.current!); clearTimeout(timeoutRef.current!); } }, [isInvalidating]); From 86bb2659b53fd60fdb7f919746eab479588e0f78 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 5 May 2025 16:07:04 -0400 Subject: [PATCH 12/16] small ui tweaks --- .../OverviewPage/components/CachingPanel.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index 719410b29..f93274659 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -14,7 +14,7 @@ export const CachingPanel = () => { const { mutateAsync: invalidateCache } = useInvalidateCache(); const { membership } = useOrgPermission(); - const hasShownSuccessRef = useRef(true); + const shouldShowSuccessRef = useRef(false); const timeoutRef = useRef(null); const [type, setType] = useState(null); @@ -34,8 +34,8 @@ export const CachingPanel = () => { }; const success = () => { - if (hasShownSuccessRef.current) return; - hasShownSuccessRef.current = true; + if (!shouldShowSuccessRef.current) return; + shouldShowSuccessRef.current = false; createNotification({ text: "Successfully invalidated cache", type: "success" }); setButtonsDisabled(false); @@ -45,7 +45,7 @@ export const CachingPanel = () => { const handleInvalidateCacheSubmit = async () => { if (!type) return; - hasShownSuccessRef.current = false; + shouldShowSuccessRef.current = true; try { await invalidateCache({ type }); @@ -67,8 +67,11 @@ export const CachingPanel = () => { useEffect(() => { refetchInvalidatingStatus(); - const timer = setTimeout(() => (hasShownSuccessRef.current = false), 1000); - return () => clearTimeout(timer); + const timer = setTimeout(() => (shouldShowSuccessRef.current = true), 1000); + return () => { + clearTimeout(timer); + setShouldPoll(false); + }; }, []); useEffect(() => { @@ -84,7 +87,7 @@ export const CachingPanel = () => { }, [isInvalidating]); useEffect(() => { - if (!hasShownSuccessRef.current && isInvalidating === false) { + if (shouldShowSuccessRef.current && isInvalidating === false) { success(); clearTimeout(timeoutRef.current!); } From 8f8236c445de9124f9e0aa81a8ac53097d704468 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 7 May 2025 01:37:26 +0530 Subject: [PATCH 13/16] feat: simplied the caching panel logic and fixed permission issue --- .../OverviewPage/components/CachingPanel.tsx | 68 +++---------------- 1 file changed, 11 insertions(+), 57 deletions(-) diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index f93274659..bb3277204 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { Badge, Button, DeleteActionModal } from "@app/components/v2"; -import { useOrgPermission } from "@app/context"; +import { useUser } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useInvalidateCache } from "@app/hooks/api"; import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries"; @@ -12,52 +12,28 @@ import { CacheType } from "@app/hooks/api/admin/types"; export const CachingPanel = () => { const { mutateAsync: invalidateCache } = useInvalidateCache(); - const { membership } = useOrgPermission(); + const { user } = useUser(); const shouldShowSuccessRef = useRef(false); - const timeoutRef = useRef(null); const [type, setType] = useState(null); - const [buttonsDisabled, setButtonsDisabled] = useState(false); const [shouldPoll, setShouldPoll] = useState(false); - const { data: isInvalidating, refetch: refetchInvalidatingStatus } = - useGetInvalidatingCacheStatus(shouldPoll); + const { data: invalidationStatus, isPending } = useGetInvalidatingCacheStatus(shouldPoll); + const isInvalidating = Boolean(shouldPoll && !isPending && invalidationStatus); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "invalidateCache" ] as const); - const disableButtonsTemporarily = () => { - setButtonsDisabled(true); - timeoutRef.current = setTimeout(() => setButtonsDisabled(false), 10000); - }; - - const success = () => { - if (!shouldShowSuccessRef.current) return; - shouldShowSuccessRef.current = false; - - createNotification({ text: "Successfully invalidated cache", type: "success" }); - setButtonsDisabled(false); - setShouldPoll(false); - }; - const handleInvalidateCacheSubmit = async () => { - if (!type) return; + if (!type || isInvalidating) return; shouldShowSuccessRef.current = true; - try { await invalidateCache({ type }); createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); - disableButtonsTemporarily(); handlePopUpClose("invalidateCache"); - - if (!(await refetchInvalidatingStatus()).data) { - success(); - return; - } - setShouldPoll(true); } catch (err) { console.error(err); @@ -66,34 +42,13 @@ export const CachingPanel = () => { }; useEffect(() => { - refetchInvalidatingStatus(); - const timer = setTimeout(() => (shouldShowSuccessRef.current = true), 1000); - return () => { - clearTimeout(timer); + if (isInvalidating) return; + + if (shouldPoll) { setShouldPoll(false); - }; - }, []); - - useEffect(() => { - if (!isInvalidating) return; - - clearTimeout(timeoutRef.current!); - setShouldPoll(true); - disableButtonsTemporarily(); - - return () => { - clearTimeout(timeoutRef.current!); - }; - }, [isInvalidating]); - - useEffect(() => { - if (shouldShowSuccessRef.current && isInvalidating === false) { - success(); - clearTimeout(timeoutRef.current!); + createNotification({ text: "Successfully invalidated cache", type: "success" }); } - }, [isInvalidating]); - - const isAdmin = membership?.role === "admin"; + }, [isInvalidating, shouldPoll]); return ( <> @@ -123,12 +78,11 @@ export const CachingPanel = () => { setType(CacheType.SECRETS); handlePopUpOpen("invalidateCache"); }} - isDisabled={!isAdmin || buttonsDisabled} + isDisabled={!user.superAdmin || isInvalidating} > Invalidate Secrets Cache
- Date: Tue, 6 May 2025 18:00:24 -0400 Subject: [PATCH 14/16] fix polling --- .../OverviewPage/components/CachingPanel.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index bb3277204..8559f7a93 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -14,12 +14,14 @@ export const CachingPanel = () => { const { mutateAsync: invalidateCache } = useInvalidateCache(); const { user } = useUser(); - const shouldShowSuccessRef = useRef(false); - const [type, setType] = useState(null); const [shouldPoll, setShouldPoll] = useState(false); - const { data: invalidationStatus, isPending } = useGetInvalidatingCacheStatus(shouldPoll); + const { + data: invalidationStatus, + isPending, + refetch + } = useGetInvalidatingCacheStatus(shouldPoll); const isInvalidating = Boolean(shouldPoll && !isPending && invalidationStatus); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -29,12 +31,16 @@ export const CachingPanel = () => { const handleInvalidateCacheSubmit = async () => { if (!type || isInvalidating) return; - shouldShowSuccessRef.current = true; try { await invalidateCache({ type }); createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); handlePopUpClose("invalidateCache"); - setShouldPoll(true); + + refetch().then((v) => + v + ? setShouldPoll(true) + : createNotification({ text: "Successfully invalidated cache", type: "success" }) + ); } catch (err) { console.error(err); createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" }); @@ -50,6 +56,10 @@ export const CachingPanel = () => { } }, [isInvalidating, shouldPoll]); + useEffect(() => { + refetch().then((v) => setShouldPoll(v.data || false)); + }, []); + return ( <>
From 334a05d5f1338928925196bd684ad7f42a33938c Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 6 May 2025 18:08:08 -0400 Subject: [PATCH 15/16] fix lint --- .../src/pages/admin/OverviewPage/components/CachingPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index 8559f7a93..39fb0e9f7 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { faRotate } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; From b37058d0e2c226eef56aceb3e9b1e57d715a68a9 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 7 May 2025 11:30:31 +0530 Subject: [PATCH 16/16] feat: switched to is fetching --- .../admin/OverviewPage/components/CachingPanel.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx index 39fb0e9f7..170a85c0d 100644 --- a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -19,10 +19,10 @@ export const CachingPanel = () => { const { data: invalidationStatus, - isPending, + isFetching, refetch } = useGetInvalidatingCacheStatus(shouldPoll); - const isInvalidating = Boolean(shouldPoll && !isPending && invalidationStatus); + const isInvalidating = Boolean(shouldPoll && (isFetching || invalidationStatus)); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "invalidateCache" @@ -34,13 +34,8 @@ export const CachingPanel = () => { try { await invalidateCache({ type }); createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); + setShouldPoll(true); handlePopUpClose("invalidateCache"); - - refetch().then((v) => - v - ? setShouldPoll(true) - : createNotification({ text: "Successfully invalidated cache", type: "success" }) - ); } catch (err) { console.error(err); createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" });