From 10939fecc06ac12746a36ff72e1a556c34786c75 Mon Sep 17 00:00:00 2001 From: x032205 Date: Sat, 28 Jun 2025 02:35:38 -0400 Subject: [PATCH 1/7] feat(super-admin): Environment Overrides --- .../20250627010508_env-overrides.ts | 21 ++ backend/src/db/schemas/super-admin.ts | 3 +- backend/src/lib/config/env.ts | 79 ++++++- backend/src/server/routes/index.ts | 4 + backend/src/server/routes/v1/admin-router.ts | 41 +++- .../super-admin/super-admin-service.ts | 72 ++++++- .../services/super-admin/super-admin-types.ts | 7 + frontend/src/hooks/api/admin/queries.ts | 14 +- frontend/src/hooks/api/admin/types.ts | 9 + frontend/src/layouts/AdminLayout/Sidebar.tsx | 5 + .../admin/EnvironmentPage/EnvironmentPage.tsx | 26 +++ .../components/EnvironmentPageForm.tsx | 193 ++++++++++++++++++ .../admin/EnvironmentPage/components/index.ts | 1 + .../src/pages/admin/EnvironmentPage/route.tsx | 25 +++ frontend/src/routeTree.gen.ts | 27 +++ frontend/src/routes.ts | 1 + 16 files changed, 521 insertions(+), 7 deletions(-) create mode 100644 backend/src/db/migrations/20250627010508_env-overrides.ts create mode 100644 frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx create mode 100644 frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx create mode 100644 frontend/src/pages/admin/EnvironmentPage/components/index.ts create mode 100644 frontend/src/pages/admin/EnvironmentPage/route.tsx diff --git a/backend/src/db/migrations/20250627010508_env-overrides.ts b/backend/src/db/migrations/20250627010508_env-overrides.ts new file mode 100644 index 000000000..535360a80 --- /dev/null +++ b/backend/src/db/migrations/20250627010508_env-overrides.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, "encryptedEnvOverrides"); + if (!hasColumn) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.binary("encryptedEnvOverrides").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides"); + if (hasColumn) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("encryptedEnvOverrides"); + }); + } +} diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index de4975b20..b5e160096 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -34,7 +34,8 @@ export const SuperAdminSchema = z.object({ encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(), encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(), encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(), - encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional() + encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional(), + encryptedEnvOverrides: zodBuffer.nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 4fb19e7bb..6ab96d3f4 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -338,8 +338,11 @@ const envSchema = z export type TEnvConfig = Readonly>; let envCfg: TEnvConfig; +let originalEnvConfig: TEnvConfig; export const getConfig = () => envCfg; +export const getOriginalConfig = () => originalEnvConfig; + // cannot import singleton logger directly as it needs config to load various transport export const initEnvConfig = (logger?: CustomLogger) => { const parsedEnv = envSchema.safeParse(process.env); @@ -349,10 +352,84 @@ export const initEnvConfig = (logger?: CustomLogger) => { process.exit(-1); } - envCfg = Object.freeze(parsedEnv.data); + const config = Object.freeze(parsedEnv.data); + envCfg = config; + + if (!originalEnvConfig) { + originalEnvConfig = config; + } + return envCfg; }; +// A list of environment variables that can be overwritten +export const overwriteSchema: { + [key: string]: { + name: string; + fields: { key: keyof z.input; description?: string }[]; + }; +} = { + google_sso: { + name: "Google SSO", + fields: [ + { + key: "CLIENT_ID_GOOGLE_LOGIN", + description: "The Client ID of your GCP OAuth2 application." + }, + { + key: "CLIENT_SECRET_GOOGLE_LOGIN", + description: "The Client Secret of your GCP OAuth2 application." + } + ] + }, + github_sso: { + name: "GitHub SSO", + fields: [ + { + key: "CLIENT_ID_GITHUB_LOGIN", + description: "The Client ID of your GitHub OAuth application." + }, + { + key: "CLIENT_SECRET_GITHUB_LOGIN", + description: "The Client Secret of your GitHub OAuth application." + } + ] + }, + github: { + name: "GitLab SSO", + fields: [ + { + key: "CLIENT_ID_GITLAB_LOGIN", + description: "The Client ID of your GitLab application." + }, + { + key: "CLIENT_SECRET_GITLAB_LOGIN", + description: "The Secret of your GitLab application." + }, + { + key: "CLIENT_GITLAB_LOGIN_URL", + description: + "The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to https://gitlab.com." + } + ] + } +}; + +const overridableKeys = new Set(Object.values(overwriteSchema).flatMap(({ fields }) => fields.map(({ key }) => key))); + +export const overrideEnvConfig = (config: Record) => { + const allowedOverrides = Object.fromEntries( + Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input)) + ); + + const tempEnv: Record = { ...process.env, ...allowedOverrides }; + const parsedResult = envSchema.safeParse(tempEnv); + + if (parsedResult.success) { + envCfg = Object.freeze(parsedResult.data); + } +}; + export const formatSmtpConfig = () => { const tlsOptions: { rejectUnauthorized: boolean; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index bde1b805e..9b4c25716 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2032,6 +2032,10 @@ export const registerRoutes = async ( cronJobs.push(adminIntegrationsSyncJob); } } + const configSyncJob = await superAdminService.initializeEnvConfigSync(); + if (configSyncJob) { + cronJobs.push(configSyncJob); + } server.decorate("store", { user: userDAL, diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index f01f1722c..8ff7f2c30 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -42,7 +42,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { encryptedGitHubAppConnectionClientSecret: true, encryptedGitHubAppConnectionSlug: true, encryptedGitHubAppConnectionId: true, - encryptedGitHubAppConnectionPrivateKey: true + encryptedGitHubAppConnectionPrivateKey: true, + encryptedEnvOverrides: true }).extend({ isMigrationModeOn: z.boolean(), defaultAuthOrgSlug: z.string().nullable(), @@ -110,7 +111,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { .refine((content) => DOMPurify.sanitize(content) === content, { message: "Page frame content contains unsafe HTML." }) - .optional() + .optional(), + envOverrides: z.record(z.string(), z.string()).optional() }), response: { 200: z.object({ @@ -381,6 +383,41 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/env-overrides", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.record( + z.string(), + z.object({ + name: z.string(), + fields: z + .object({ + key: z.string(), + value: z.string(), + hasEnvEntry: z.boolean(), + description: z.string().optional() + }) + .array() + }) + ) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + const envOverrides = await server.services.superAdmin.getEnvOverridesOrganized(); + return envOverrides; + } + }); + server.route({ method: "DELETE", url: "/user-management/users/:userId", diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8a0d4dd14..a179e646c 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -5,7 +5,7 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { getConfig, getOriginalConfig, overrideEnvConfig, overwriteSchema } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -33,6 +33,7 @@ import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { CacheType, + EnvOverrides, LoginMethod, TAdminBootstrapInstanceDTO, TAdminGetIdentitiesDTO, @@ -234,6 +235,45 @@ export const superAdminServiceFactory = ({ adminIntegrationsConfig = config; }; + const getEnvOverrides = async () => { + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + + if (!serverCfg || !serverCfg.encryptedEnvOverrides) { + return {}; + } + + const decrypt = kmsService.decryptWithRootKey(); + + const overrides = JSON.parse(decrypt(serverCfg.encryptedEnvOverrides).toString()) as Record; + + return overrides; + }; + + const getEnvOverridesOrganized = async (): Promise => { + const overrides = await getEnvOverrides(); + const ogConfig = getOriginalConfig(); + + return Object.fromEntries( + Object.entries(overwriteSchema).map(([groupKey, groupDef]) => [ + groupKey, + { + name: groupDef.name, + fields: groupDef.fields.map(({ key, description }) => ({ + key, + description, + value: overrides[key] || "", + hasEnvEntry: !!(ogConfig as unknown as Record)[key] + })) + } + ]) + ); + }; + + const $syncEnvConfig = async () => { + const config = await getEnvOverrides(); + overrideEnvConfig(config); + }; + const updateServerCfg = async ( data: TSuperAdminUpdate & { slackClientId?: string; @@ -246,6 +286,7 @@ export const superAdminServiceFactory = ({ gitHubAppConnectionSlug?: string; gitHubAppConnectionId?: string; gitHubAppConnectionPrivateKey?: string; + envOverrides?: Record; }, userId: string ) => { @@ -374,6 +415,14 @@ export const superAdminServiceFactory = ({ gitHubAppConnectionSettingsUpdated = true; } + let envOverridesUpdated = false; + if (data.envOverrides !== undefined) { + const encryptedEnvOverrides = encryptWithRoot(Buffer.from(JSON.stringify(data.envOverrides))); + updatedData.encryptedEnvOverrides = encryptedEnvOverrides; + updatedData.envOverrides = undefined; + envOverridesUpdated = true; + } + const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData); await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); @@ -382,6 +431,10 @@ export const superAdminServiceFactory = ({ await $syncAdminIntegrationConfig(); } + if (envOverridesUpdated) { + await $syncEnvConfig(); + } + if ( updatedServerCfg.encryptedMicrosoftTeamsAppId && updatedServerCfg.encryptedMicrosoftTeamsClientSecret && @@ -814,6 +867,18 @@ export const superAdminServiceFactory = ({ return job; }; + const initializeEnvConfigSync = async () => { + logger.info("Setting up background sync process for environment overrides"); + + await $syncEnvConfig(); + + // sync every 5 minutes + const job = new CronJob("*/5 * * * *", $syncEnvConfig); + job.start(); + + return job; + }; + return { initServerCfg, updateServerCfg, @@ -833,6 +898,9 @@ export const superAdminServiceFactory = ({ getOrganizations, deleteOrganization, deleteOrganizationMembership, - initializeAdminIntegrationConfigSync + initializeAdminIntegrationConfigSync, + initializeEnvConfigSync, + getEnvOverrides, + getEnvOverridesOrganized }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 205c59f2c..3e5ca349c 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -74,3 +74,10 @@ export type TAdminIntegrationConfig = { privateKey: string; }; }; + +export interface EnvOverrides { + [key: string]: { + name: string; + fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[]; + }; +} diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index c628df955..871c7288c 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -10,6 +10,7 @@ import { AdminGetUsersFilters, AdminIntegrationsConfig, OrganizationWithProjects, + TGetEnvOverrides, TGetInvalidatingCacheStatus, TGetServerRootKmsEncryptionDetails, TServerConfig @@ -31,7 +32,8 @@ export const adminQueryKeys = { getAdminSlackConfig: () => ["admin-slack-config"] as const, getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const, getInvalidateCache: () => ["admin-invalidate-cache"] as const, - getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const + getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const, + getEnvOverrides: () => ["env-overrides"] as const }; export const fetchServerConfig = async () => { @@ -163,3 +165,13 @@ export const useGetInvalidatingCacheStatus = (enabled = true) => { refetchInterval: (data) => (data ? 3000 : false) }); }; + +export const useGetEnvOverrides = () => { + return useQuery({ + queryKey: adminQueryKeys.getEnvOverrides(), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/admin/env-overrides"); + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index c5d92b9da..4580c6581 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -48,6 +48,7 @@ export type TServerConfig = { authConsentContent?: string; pageFrameContent?: string; invalidatingCache: boolean; + envOverrides?: Record; }; export type TUpdateServerConfigDTO = { @@ -61,6 +62,7 @@ export type TUpdateServerConfigDTO = { gitHubAppConnectionSlug?: string; gitHubAppConnectionId?: string; gitHubAppConnectionPrivateKey?: string; + envOverrides?: Record; } & Partial; export type TCreateAdminUserDTO = { @@ -138,3 +140,10 @@ export type TInvalidateCacheDTO = { export type TGetInvalidatingCacheStatus = { invalidating: boolean; }; + +export interface TGetEnvOverrides { + [key: string]: { + name: string; + fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[]; + }; +} diff --git a/frontend/src/layouts/AdminLayout/Sidebar.tsx b/frontend/src/layouts/AdminLayout/Sidebar.tsx index ea938394b..db6384e68 100644 --- a/frontend/src/layouts/AdminLayout/Sidebar.tsx +++ b/frontend/src/layouts/AdminLayout/Sidebar.tsx @@ -41,6 +41,11 @@ const generalTabs = [ label: "Caching", icon: "note", link: "/admin/caching" + }, + { + label: "Environment", + icon: "unlock", + link: "/admin/environment" } ]; diff --git a/frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx b/frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx new file mode 100644 index 000000000..668902c76 --- /dev/null +++ b/frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx @@ -0,0 +1,26 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { PageHeader } from "@app/components/v2"; +import { EnvironmentPageForm } from "./components"; + +export const EnvironmentPage = () => { + const { t } = useTranslation(); + + return ( +
+ + {t("common.head-title", { title: "Admin" })} + +
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx new file mode 100644 index 000000000..b120d49e0 --- /dev/null +++ b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx @@ -0,0 +1,193 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Controller, useForm, useWatch } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faExclamationTriangle, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api"; + +type TForm = Record; + +export const EnvironmentPageForm = () => { + const { data: envOverrides } = useGetEnvOverrides(); + const { mutateAsync: updateServerConfig } = useUpdateServerConfig(); + const [search, setSearch] = useState(""); + + const allFields = useMemo(() => { + if (!envOverrides) return []; + return Object.values(envOverrides).flatMap((group) => group.fields); + }, [envOverrides]); + + const formSchema = useMemo(() => { + return z.object(Object.fromEntries(allFields.map((field) => [field.key, z.string()]))); + }, [allFields]); + + const defaultValues = useMemo(() => { + const values: Record = {}; + allFields.forEach((field) => { + values[field.key] = field.value ?? ""; + }); + return values; + }, [allFields]); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues + }); + + const formValues = useWatch({ control }); + + const filteredData = useMemo(() => { + if (!envOverrides) return []; + + const searchTerm = search.toLowerCase().trim(); + if (!searchTerm) { + return Object.values(envOverrides); + } + + return Object.values(envOverrides) + .map((group) => { + const filteredFields = group.fields.filter((field) => { + const value = formValues?.[field.key] ?? ""; + return ( + field.key.toLowerCase().includes(searchTerm) || + (field.description ?? "").toLowerCase().includes(searchTerm) || + value.toLowerCase().includes(searchTerm) + ); + }); + + if (filteredFields.length > 0) { + return { ...group, fields: filteredFields }; + } + return null; + }) + .filter(Boolean); + }, [search, formValues, envOverrides]); + + useEffect(() => { + reset(defaultValues); + }, [defaultValues, reset]); + + const onSubmit = useCallback( + async (formData: TForm) => { + try { + const filteredFormData = Object.fromEntries( + Object.entries(formData).filter(([, value]) => value !== "") + ); + await updateServerConfig({ + envOverrides: filteredFormData + }); + + createNotification({ + type: "success", + text: "Environment overrides updated successfully" + }); + + reset(formData); + } catch { + createNotification({ + type: "error", + text: "Failed to update environment overrides" + }); + } + }, + [reset, updateServerConfig] + ); + + return ( + <> +
+
+
+
+

Overrides

+
+

Override specific environment variables.

+
+ +
+ +
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search for keys, descriptions, and values..." + className="flex-1" + /> +
+ {filteredData.map((group) => ( +
+ {group!.name} +
+
+ {group!.fields.map((field, i) => ( +
+
+ {field.key} + {field.description} +
+ +
+ {field.hasEnvEntry && ( + + + + )} + + ( + + + + )} + /> +
+
+ ))} +
+
+
+ ))} +
+ + + ); +}; diff --git a/frontend/src/pages/admin/EnvironmentPage/components/index.ts b/frontend/src/pages/admin/EnvironmentPage/components/index.ts new file mode 100644 index 000000000..44b82c206 --- /dev/null +++ b/frontend/src/pages/admin/EnvironmentPage/components/index.ts @@ -0,0 +1 @@ +export { EnvironmentPageForm } from "./EnvironmentPageForm"; diff --git a/frontend/src/pages/admin/EnvironmentPage/route.tsx b/frontend/src/pages/admin/EnvironmentPage/route.tsx new file mode 100644 index 000000000..0b28b44cb --- /dev/null +++ b/frontend/src/pages/admin/EnvironmentPage/route.tsx @@ -0,0 +1,25 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { EnvironmentPage } from "./EnvironmentPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/admin/_admin-layout/environment" +)({ + component: EnvironmentPage, + beforeLoad: async () => { + return { + breadcrumbs: [ + { + label: "Admin", + link: linkOptions({ to: "/admin" }) + }, + { + label: "Environment", + link: linkOptions({ + to: "/admin/environment" + }) + } + ] + }; + } +}); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index a2aa9548b..02a3e30d9 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -43,6 +43,7 @@ import { Route as authProviderSuccessPageRouteImport } from './pages/auth/Provid import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route' import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route' import { Route as adminIntegrationsPageRouteImport } from './pages/admin/IntegrationsPage/route' +import { Route as adminEnvironmentPageRouteImport } from './pages/admin/EnvironmentPage/route' import { Route as adminEncryptionPageRouteImport } from './pages/admin/EncryptionPage/route' import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/route' import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route' @@ -607,6 +608,12 @@ const adminIntegrationsPageRouteRoute = adminIntegrationsPageRouteImport.update( } as any, ) +const adminEnvironmentPageRouteRoute = adminEnvironmentPageRouteImport.update({ + id: '/environment', + path: '/environment', + getParentRoute: () => adminLayoutRoute, +} as any) + const adminEncryptionPageRouteRoute = adminEncryptionPageRouteImport.update({ id: '/encryption', path: '/encryption', @@ -2353,6 +2360,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof adminEncryptionPageRouteImport parentRoute: typeof adminLayoutImport } + '/_authenticate/_inject-org-details/admin/_admin-layout/environment': { + id: '/_authenticate/_inject-org-details/admin/_admin-layout/environment' + path: '/environment' + fullPath: '/admin/environment' + preLoaderRoute: typeof adminEnvironmentPageRouteImport + parentRoute: typeof adminLayoutImport + } '/_authenticate/_inject-org-details/admin/_admin-layout/integrations': { id: '/_authenticate/_inject-org-details/admin/_admin-layout/integrations' path: '/integrations' @@ -4484,6 +4498,7 @@ interface adminLayoutRouteChildren { adminAuthenticationPageRouteRoute: typeof adminAuthenticationPageRouteRoute adminCachingPageRouteRoute: typeof adminCachingPageRouteRoute adminEncryptionPageRouteRoute: typeof adminEncryptionPageRouteRoute + adminEnvironmentPageRouteRoute: typeof adminEnvironmentPageRouteRoute adminIntegrationsPageRouteRoute: typeof adminIntegrationsPageRouteRoute adminMachineIdentitiesResourcesPageRouteRoute: typeof adminMachineIdentitiesResourcesPageRouteRoute adminOrganizationResourcesPageRouteRoute: typeof adminOrganizationResourcesPageRouteRoute @@ -4495,6 +4510,7 @@ const adminLayoutRouteChildren: adminLayoutRouteChildren = { adminAuthenticationPageRouteRoute: adminAuthenticationPageRouteRoute, adminCachingPageRouteRoute: adminCachingPageRouteRoute, adminEncryptionPageRouteRoute: adminEncryptionPageRouteRoute, + adminEnvironmentPageRouteRoute: adminEnvironmentPageRouteRoute, adminIntegrationsPageRouteRoute: adminIntegrationsPageRouteRoute, adminMachineIdentitiesResourcesPageRouteRoute: adminMachineIdentitiesResourcesPageRouteRoute, @@ -4697,6 +4713,7 @@ export interface FileRoutesByFullPath { '/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/caching': typeof adminCachingPageRouteRoute '/admin/encryption': typeof adminEncryptionPageRouteRoute + '/admin/environment': typeof adminEnvironmentPageRouteRoute '/admin/integrations': typeof adminIntegrationsPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren @@ -4918,6 +4935,7 @@ export interface FileRoutesByTo { '/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/caching': typeof adminCachingPageRouteRoute '/admin/encryption': typeof adminEncryptionPageRouteRoute + '/admin/environment': typeof adminEnvironmentPageRouteRoute '/admin/integrations': typeof adminIntegrationsPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren @@ -5139,6 +5157,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/caching': typeof adminCachingPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/encryption': typeof adminEncryptionPageRouteRoute + '/_authenticate/_inject-org-details/admin/_admin-layout/environment': typeof adminEnvironmentPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren @@ -5371,6 +5390,7 @@ export interface FileRouteTypes { | '/admin/authentication' | '/admin/caching' | '/admin/encryption' + | '/admin/environment' | '/admin/integrations' | '/cert-manager/$projectId' | '/kms/$projectId' @@ -5591,6 +5611,7 @@ export interface FileRouteTypes { | '/admin/authentication' | '/admin/caching' | '/admin/encryption' + | '/admin/environment' | '/admin/integrations' | '/cert-manager/$projectId' | '/kms/$projectId' @@ -5810,6 +5831,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/admin/_admin-layout/authentication' | '/_authenticate/_inject-org-details/admin/_admin-layout/caching' | '/_authenticate/_inject-org-details/admin/_admin-layout/encryption' + | '/_authenticate/_inject-org-details/admin/_admin-layout/environment' | '/_authenticate/_inject-org-details/admin/_admin-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId' @@ -6267,6 +6289,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/admin/_admin-layout/authentication", "/_authenticate/_inject-org-details/admin/_admin-layout/caching", "/_authenticate/_inject-org-details/admin/_admin-layout/encryption", + "/_authenticate/_inject-org-details/admin/_admin-layout/environment", "/_authenticate/_inject-org-details/admin/_admin-layout/integrations", "/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities", "/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations", @@ -6309,6 +6332,10 @@ export const routeTree = rootRoute "filePath": "admin/EncryptionPage/route.tsx", "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" }, + "/_authenticate/_inject-org-details/admin/_admin-layout/environment": { + "filePath": "admin/EnvironmentPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" + }, "/_authenticate/_inject-org-details/admin/_admin-layout/integrations": { "filePath": "admin/IntegrationsPage/route.tsx", "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 4937e7226..973e36fff 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -8,6 +8,7 @@ const adminRoute = route("/admin", [ index("admin/GeneralPage/route.tsx"), route("/encryption", "admin/EncryptionPage/route.tsx"), route("/authentication", "admin/AuthenticationPage/route.tsx"), + route("/environment", "admin/EnvironmentPage/route.tsx"), route("/integrations", "admin/IntegrationsPage/route.tsx"), route("/caching", "admin/CachingPage/route.tsx"), route("/resources/organizations", "admin/OrganizationResourcesPage/route.tsx"), From a389ede03d0ff5c7cf855077de48a40da7624ba2 Mon Sep 17 00:00:00 2001 From: x032205 Date: Sat, 28 Jun 2025 03:01:34 -0400 Subject: [PATCH 2/7] Review fixes --- backend/src/lib/config/env.ts | 8 +- backend/src/server/routes/v1/admin-router.ts | 8 +- .../cloudflare-pages-sync-router.ts | 5 +- .../cloudflare-connection-schema.ts | 2 +- .../services/super-admin/super-admin-types.ts | 4 +- .../MinimizedOrgSidebar.tsx | 5 +- .../admin/EnvironmentPage/EnvironmentPage.tsx | 1 + .../components/EnvironmentPageForm.tsx | 173 +++++++++--------- 8 files changed, 106 insertions(+), 100 deletions(-) diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 6ab96d3f4..99514556b 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -366,7 +366,7 @@ export const initEnvConfig = (logger?: CustomLogger) => { export const overwriteSchema: { [key: string]: { name: string; - fields: { key: keyof z.input; description?: string }[]; + fields: { key: keyof TEnvConfig; description?: string }[]; }; } = { google_sso: { @@ -395,7 +395,7 @@ export const overwriteSchema: { } ] }, - github: { + gitlab_sso: { name: "GitLab SSO", fields: [ { @@ -415,7 +415,9 @@ export const overwriteSchema: { } }; -const overridableKeys = new Set(Object.values(overwriteSchema).flatMap(({ fields }) => fields.map(({ key }) => key))); +export const overridableKeys = new Set( + Object.values(overwriteSchema).flatMap(({ fields }) => fields.map(({ key }) => key)) +); export const overrideEnvConfig = (config: Record) => { const allowedOverrides = Object.fromEntries( diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 8ff7f2c30..e35ac7d50 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -8,7 +8,7 @@ import { SuperAdminSchema, UsersSchema } from "@app/db/schemas"; -import { getConfig } from "@app/lib/config/env"; +import { getConfig, overridableKeys } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; @@ -112,11 +112,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { message: "Page frame content contains unsafe HTML." }) .optional(), - envOverrides: z.record(z.string(), z.string()).optional() + envOverrides: z.record(z.enum(Array.from(overridableKeys) as [string, ...string[]]), z.string()).optional() }), response: { 200: z.object({ - config: SuperAdminSchema.extend({ + config: SuperAdminSchema.omit({ + encryptedEnvOverrides: true + }).extend({ defaultAuthOrgSlug: z.string().nullable() }) }) diff --git a/backend/src/server/routes/v1/secret-sync-routers/cloudflare-pages-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/cloudflare-pages-sync-router.ts index 6a70b5837..59b95323c 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/cloudflare-pages-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/cloudflare-pages-sync-router.ts @@ -1,10 +1,11 @@ -import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; -import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; import { CloudflarePagesSyncSchema, CreateCloudflarePagesSyncSchema, UpdateCloudflarePagesSyncSchema } from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; export const registerCloudflarePagesSyncRouter = async (server: FastifyZodProvider) => registerSyncSecretsEndpoints({ diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-schema.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-schema.ts index 64dee9dd5..f3b26a2d6 100644 --- a/backend/src/services/app-connection/cloudflare/cloudflare-connection-schema.ts +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-schema.ts @@ -1,6 +1,7 @@ import z from "zod"; import { AppConnections } from "@app/lib/api-docs"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { BaseAppConnectionSchema, @@ -9,7 +10,6 @@ import { } from "@app/services/app-connection/app-connection-schemas"; import { CloudflareConnectionMethod } from "./cloudflare-connection-enum"; -import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; const accountIdCharacterValidator = characterValidator([ CharacterType.AlphaNumeric, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 3e5ca349c..b57a015a4 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -1,3 +1,5 @@ +import { TEnvConfig } from "@app/lib/config/env"; + export type TAdminSignUpDTO = { email: string; password: string; @@ -78,6 +80,6 @@ export type TAdminIntegrationConfig = { export interface EnvOverrides { [key: string]: { name: string; - fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[]; + fields: { key: keyof TEnvConfig; value: string; hasEnvEntry: boolean; description?: string }[]; }; } diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index a005c1359..f4ce9bc40 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -164,7 +164,10 @@ export const MinimizedOrgSidebar = () => { const handleCopyToken = async () => { try { await window.navigator.clipboard.writeText(getAuthToken()); - createNotification({ type: "success", text: "Copied current login session token to clipboard" }); + createNotification({ + type: "success", + text: "Copied current login session token to clipboard" + }); } catch (error) { console.log(error); createNotification({ type: "error", text: "Failed to copy user token to clipboard" }); diff --git a/frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx b/frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx index 668902c76..ea4467131 100644 --- a/frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx +++ b/frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx @@ -2,6 +2,7 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { PageHeader } from "@app/components/v2"; + import { EnvironmentPageForm } from "./components"; export const EnvironmentPage = () => { diff --git a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx index b120d49e0..8e07e37be 100644 --- a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx +++ b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx @@ -1,12 +1,12 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Controller, useForm, useWatch } from "react-hook-form"; +import { faExclamationTriangle, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faExclamationTriangle, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api"; type TForm = Record; @@ -55,14 +55,11 @@ export const EnvironmentPageForm = () => { return Object.values(envOverrides) .map((group) => { - const filteredFields = group.fields.filter((field) => { - const value = formValues?.[field.key] ?? ""; - return ( + const filteredFields = group.fields.filter( + (field) => field.key.toLowerCase().includes(searchTerm) || - (field.description ?? "").toLowerCase().includes(searchTerm) || - value.toLowerCase().includes(searchTerm) - ); - }); + (field.description ?? "").toLowerCase().includes(searchTerm) + ); if (filteredFields.length > 0) { return { ...group, fields: filteredFields }; @@ -103,91 +100,89 @@ export const EnvironmentPageForm = () => { ); return ( - <> -
-
-
-
-

Overrides

-
-

Override specific environment variables.

-
- -
- + +
+
+
+

Overrides

+

Override specific environment variables.

- setSearch(e.target.value)} - leftIcon={} - placeholder="Search for keys, descriptions, and values..." - className="flex-1" - /> -
- {filteredData.map((group) => ( -
- {group!.name} -
-
- {group!.fields.map((field, i) => ( -
-
- {field.key} - {field.description} -
-
- {field.hasEnvEntry && ( - - - - )} - - ( - - - - )} - /> -
+
+ +
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search for keys, descriptions, and values..." + className="flex-1" + /> +
+ {filteredData.map((group) => ( +
+ {group!.name} +
+
+ {group!.fields.map((field, i) => ( +
+
+ {field.key} + {field.description}
- ))} -
+ +
+ {field.hasEnvEntry && ( + + + + )} + + ( + + + + )} + /> +
+
+ ))}
- ))} -
- - +
+ ))} +
+ ); }; From cb6c28ac26a7b6ccf12b9d67d57d3e1f8863abb5 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 30 Jun 2025 14:08:27 -0400 Subject: [PATCH 3/7] UI updates --- .../v2/HighlightText/HighlightText.tsx | 43 +++++ .../src/components/v2/HighlightText/index.tsx | 1 + .../components/EnvironmentPageForm.tsx | 156 ++++++++++++------ 3 files changed, 148 insertions(+), 52 deletions(-) create mode 100644 frontend/src/components/v2/HighlightText/HighlightText.tsx create mode 100644 frontend/src/components/v2/HighlightText/index.tsx diff --git a/frontend/src/components/v2/HighlightText/HighlightText.tsx b/frontend/src/components/v2/HighlightText/HighlightText.tsx new file mode 100644 index 000000000..9b1c525eb --- /dev/null +++ b/frontend/src/components/v2/HighlightText/HighlightText.tsx @@ -0,0 +1,43 @@ +export const HighlightText = ({ + text, + highlight, + highlightClassName +}: { + text: string | undefined | null; + highlight: string; + highlightClassName?: string; +}) => { + if (!text) return null; + const searchTerm = highlight.toLowerCase().trim(); + + if (!searchTerm) return {text}; + + const parts: React.ReactNode[] = []; + let lastIndex = 0; + + const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(escapedSearchTerm, "gi"); + + while (true) { + const match = regex.exec(text); + if (match === null) break; + + if (match.index > lastIndex) { + parts.push({text.substring(lastIndex, match.index)}); + } + + parts.push( + + {text.substring(match.index, match.index + match[0].length)} + + ); + + lastIndex = regex.lastIndex; + } + + if (lastIndex < text.length) { + parts.push({text.substring(lastIndex)}); + } + + return parts; +}; diff --git a/frontend/src/components/v2/HighlightText/index.tsx b/frontend/src/components/v2/HighlightText/index.tsx new file mode 100644 index 000000000..a2ff1b504 --- /dev/null +++ b/frontend/src/components/v2/HighlightText/index.tsx @@ -0,0 +1 @@ +export { HighlightText } from "./HighlightText"; diff --git a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx index 8e07e37be..f2c50a3f5 100644 --- a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx +++ b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx @@ -1,16 +1,116 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { Controller, useForm, useWatch } from "react-hook-form"; -import { faExclamationTriangle, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { Control, Controller, useForm, useWatch } from "react-hook-form"; +import { + faChevronRight, + faExclamationTriangle, + faMagnifyingGlass +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2"; +import { HighlightText } from "@app/components/v2/HighlightText"; import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api"; type TForm = Record; +export const GroupContainer = ({ + group, + control, + search +}: { + group: { + fields: { + key: string; + value: string; + hasEnvEntry: boolean; + description?: string; + }[]; + name: string; + }; + control: Control; + search: string; +}) => { + const [open, setOpen] = useState(false); + + return ( +
+
setOpen((o) => !o)} + onKeyDown={(e) => { + if (e.key === "Enter") { + setOpen((o) => !o); + } + }} + > + + +
{group.name}
+
+ + {(open || search) && ( +
+ {group.fields.map((field) => ( +
+
+ + + + + + +
+ +
+ {field.hasEnvEntry && ( + + + + )} + + ( + + + + )} + /> +
+
+ ))} +
+ )} +
+ ); +}; + export const EnvironmentPageForm = () => { const { data: envOverrides } = useGetEnvOverrides(); const { mutateAsync: updateServerConfig } = useUpdateServerConfig(); @@ -130,57 +230,9 @@ export const EnvironmentPageForm = () => { placeholder="Search for keys, descriptions, and values..." className="flex-1" /> -
+
{filteredData.map((group) => ( -
- {group!.name} -
-
- {group!.fields.map((field, i) => ( -
-
- {field.key} - {field.description} -
- -
- {field.hasEnvEntry && ( - - - - )} - - ( - - - - )} - /> -
-
- ))} -
-
-
+ ))}
From d531d069d14a67351c6ef6be397190c0ffed2423 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 1 Jul 2025 11:23:44 -0400 Subject: [PATCH 4/7] Add azure app connection --- backend/src/lib/config/env.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index b9be95fea..98a75cc10 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -372,6 +372,19 @@ export const overwriteSchema: { fields: { key: keyof TEnvConfig; description?: string }[]; }; } = { + azure: { + name: "Azure", + fields: [ + { + key: "INF_APP_CONNECTION_AZURE_CLIENT_ID", + description: "The Application (Client) ID of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET", + description: "The Client Secret of your Azure application." + } + ] + }, google_sso: { name: "Google SSO", fields: [ From 4b06880320ac70b9052523f3802e35c3c9cbec3f Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 1 Jul 2025 11:52:01 -0400 Subject: [PATCH 5/7] Feedback fixes --- backend/src/lib/config/env.ts | 17 +++++++++++++++++ .../services/super-admin/super-admin-service.ts | 11 ++++++++++- .../v2/HighlightText/HighlightText.tsx | 9 +++------ .../components/EnvironmentPageForm.tsx | 9 +++++++-- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 98a75cc10..2523c763c 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { QueueWorkerProfile } from "@app/lib/types"; +import { BadRequestError } from "../errors"; import { removeTrailingSlash } from "../fn"; import { CustomLogger } from "../logger/logger"; import { zpStr } from "../zod"; @@ -435,6 +436,22 @@ export const overridableKeys = new Set( Object.values(overwriteSchema).flatMap(({ fields }) => fields.map(({ key }) => key)) ); +export const validateOverrides = (config: Record) => { + const allowedOverrides = Object.fromEntries( + Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input)) + ); + + const tempEnv: Record = { ...process.env, ...allowedOverrides }; + const parsedResult = envSchema.safeParse(tempEnv); + + if (!parsedResult.success) { + const errorDetails = parsedResult.error.issues + .map((issue) => `Key: "${issue.path.join(".")}", Error: ${issue.message}`) + .join("\n"); + throw new BadRequestError({ message: errorDetails }); + } +}; + export const overrideEnvConfig = (config: Record) => { const allowedOverrides = Object.fromEntries( Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input)) diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index a179e646c..2ca7a0c33 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -5,7 +5,13 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig, getOriginalConfig, overrideEnvConfig, overwriteSchema } from "@app/lib/config/env"; +import { + getConfig, + getOriginalConfig, + overrideEnvConfig, + overwriteSchema, + validateOverrides +} from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -417,6 +423,9 @@ export const superAdminServiceFactory = ({ let envOverridesUpdated = false; if (data.envOverrides !== undefined) { + // Verify input format + validateOverrides(data.envOverrides); + const encryptedEnvOverrides = encryptWithRoot(Buffer.from(JSON.stringify(data.envOverrides))); updatedData.encryptedEnvOverrides = encryptedEnvOverrides; updatedData.envOverrides = undefined; diff --git a/frontend/src/components/v2/HighlightText/HighlightText.tsx b/frontend/src/components/v2/HighlightText/HighlightText.tsx index 9b1c525eb..7f7bd6bee 100644 --- a/frontend/src/components/v2/HighlightText/HighlightText.tsx +++ b/frontend/src/components/v2/HighlightText/HighlightText.tsx @@ -18,21 +18,18 @@ export const HighlightText = ({ const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(escapedSearchTerm, "gi"); - while (true) { - const match = regex.exec(text); - if (match === null) break; - + for (const match of text.matchAll(regex)) { if (match.index > lastIndex) { parts.push({text.substring(lastIndex, match.index)}); } parts.push( - {text.substring(match.index, match.index + match[0].length)} + {match[0]} ); - lastIndex = regex.lastIndex; + lastIndex = match.index + match[0].length; } if (lastIndex < text.length) { diff --git a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx index f2c50a3f5..70cef790d 100644 --- a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx +++ b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx @@ -189,10 +189,15 @@ export const EnvironmentPageForm = () => { }); reset(formData); - } catch { + } catch (error) { + const errorMessage = + (error as any)?.response?.data?.message || + (error as any)?.message || + "An unknown error occurred"; createNotification({ type: "error", - text: "Failed to update environment overrides" + title: "Failed to update environment overrides", + text: errorMessage }); } }, From 8aa26b77edbce6b041d5bd82c022823a4bfa930f Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 1 Jul 2025 13:11:15 -0400 Subject: [PATCH 6/7] Fix check --- .../v2/HighlightText/HighlightText.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/v2/HighlightText/HighlightText.tsx b/frontend/src/components/v2/HighlightText/HighlightText.tsx index 7f7bd6bee..3ce7c8f19 100644 --- a/frontend/src/components/v2/HighlightText/HighlightText.tsx +++ b/frontend/src/components/v2/HighlightText/HighlightText.tsx @@ -18,19 +18,21 @@ export const HighlightText = ({ const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(escapedSearchTerm, "gi"); - for (const match of text.matchAll(regex)) { - if (match.index > lastIndex) { - parts.push({text.substring(lastIndex, match.index)}); + text.replace(regex, (match: string, offset: number) => { + if (offset > lastIndex) { + parts.push({text.substring(lastIndex, offset)}); } parts.push( - - {match[0]} + + {match} ); - lastIndex = match.index + match[0].length; - } + lastIndex = offset + match.length; + + return match; + }); if (lastIndex < text.length) { parts.push({text.substring(lastIndex)}); From 7134e1dc66b326346e32c20047b186fd57d8aca8 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 2 Jul 2025 02:18:04 +0800 Subject: [PATCH 7/7] misc: updated success notif --- .../admin/EnvironmentPage/components/EnvironmentPageForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx index 70cef790d..2a629bf95 100644 --- a/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx +++ b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx @@ -185,7 +185,7 @@ export const EnvironmentPageForm = () => { createNotification({ type: "success", - text: "Environment overrides updated successfully" + text: "Environment overrides updated successfully. It can take up to 5 minutes to take effect." }); reset(formData);