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 8db5c16c4..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"; @@ -341,8 +342,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); @@ -352,10 +356,115 @@ 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 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: [ + { + 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." + } + ] + }, + gitlab_sso: { + 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." + } + ] + } +}; + +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)) + ); + + 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 aeef89838..401eeaf94 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2045,6 +2045,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 a21587db1..81e911621 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"; @@ -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,11 +111,14 @@ 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.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() }) }) @@ -381,6 +385,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..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 } 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"; @@ -33,6 +39,7 @@ import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { CacheType, + EnvOverrides, LoginMethod, TAdminBootstrapInstanceDTO, TAdminGetIdentitiesDTO, @@ -234,6 +241,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 +292,7 @@ export const superAdminServiceFactory = ({ gitHubAppConnectionSlug?: string; gitHubAppConnectionId?: string; gitHubAppConnectionPrivateKey?: string; + envOverrides?: Record; }, userId: string ) => { @@ -374,6 +421,17 @@ export const superAdminServiceFactory = ({ gitHubAppConnectionSettingsUpdated = true; } + 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; + 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 +440,10 @@ export const superAdminServiceFactory = ({ await $syncAdminIntegrationConfig(); } + if (envOverridesUpdated) { + await $syncEnvConfig(); + } + if ( updatedServerCfg.encryptedMicrosoftTeamsAppId && updatedServerCfg.encryptedMicrosoftTeamsClientSecret && @@ -814,6 +876,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 +907,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..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; @@ -74,3 +76,10 @@ export type TAdminIntegrationConfig = { privateKey: string; }; }; + +export interface EnvOverrides { + [key: string]: { + name: string; + fields: { key: keyof TEnvConfig; value: string; hasEnvEntry: boolean; description?: string }[]; + }; +} diff --git a/frontend/src/components/v2/HighlightText/HighlightText.tsx b/frontend/src/components/v2/HighlightText/HighlightText.tsx new file mode 100644 index 000000000..3ce7c8f19 --- /dev/null +++ b/frontend/src/components/v2/HighlightText/HighlightText.tsx @@ -0,0 +1,42 @@ +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"); + + text.replace(regex, (match: string, offset: number) => { + if (offset > lastIndex) { + parts.push({text.substring(lastIndex, offset)}); + } + + parts.push( + + {match} + + ); + + lastIndex = offset + match.length; + + return match; + }); + + 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/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..ea4467131 --- /dev/null +++ b/frontend/src/pages/admin/EnvironmentPage/EnvironmentPage.tsx @@ -0,0 +1,27 @@ +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..2a629bf95 --- /dev/null +++ b/frontend/src/pages/admin/EnvironmentPage/components/EnvironmentPageForm.tsx @@ -0,0 +1,245 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +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(); + 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) => + field.key.toLowerCase().includes(searchTerm) || + (field.description ?? "").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. It can take up to 5 minutes to take effect." + }); + + reset(formData); + } catch (error) { + const errorMessage = + (error as any)?.response?.data?.message || + (error as any)?.message || + "An unknown error occurred"; + createNotification({ + type: "error", + title: "Failed to update environment overrides", + text: errorMessage + }); + } + }, + [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) => ( + + ))} +
+ + ); +}; 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"),