diff --git a/backend/src/db/migrations/20250226082254_add-gov-banner-and-consent-fields.ts b/backend/src/db/migrations/20250226082254_add-gov-banner-and-consent-fields.ts new file mode 100644 index 000000000..2a159af56 --- /dev/null +++ b/backend/src/db/migrations/20250226082254_add-gov-banner-and-consent-fields.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent"); + const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent"); + if (await knex.schema.hasTable(TableName.SuperAdmin)) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (!hasAuthConsentContentCol) { + t.text("authConsentContent"); + } + if (!hasPageFrameContentCol) { + t.text("pageFrameContent"); + } + }); + } +} + +export async function down(knex: Knex): Promise { + const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent"); + const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent"); + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (hasAuthConsentContentCol) { + t.dropColumn("authConsentContent"); + } + if (hasPageFrameContentCol) { + t.dropColumn("pageFrameContent"); + } + }); +} diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index edab3a0e9..2c0fd7dc4 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -23,7 +23,9 @@ export const SuperAdminSchema = z.object({ defaultAuthOrgId: z.string().uuid().nullable().optional(), enabledLoginMethods: z.string().array().nullable().optional(), encryptedSlackClientId: zodBuffer.nullable().optional(), - encryptedSlackClientSecret: zodBuffer.nullable().optional() + encryptedSlackClientSecret: zodBuffer.nullable().optional(), + authConsentContent: z.string().nullable().optional(), + pageFrameContent: z.string().nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 6ecebb274..9b11677a2 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -72,7 +72,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { message: "At least one login method should be enabled." }), slackClientId: z.string().optional(), - slackClientSecret: z.string().optional() + slackClientSecret: z.string().optional(), + authConsentContent: z.string().optional(), + pageFrameContent: z.string().optional() }), response: { 200: z.object({ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3a9482a56..bb9a01f93 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -54,6 +54,7 @@ "classnames": "^2.5.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", + "dompurify": "^3.2.4", "file-saver": "^2.0.5", "framer-motion": "^11.14.1", "i18next": "^24.1.0", @@ -4078,6 +4079,12 @@ "@types/react": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", @@ -5945,11 +5952,12 @@ } }, "node_modules/dompurify": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", - "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optional": true + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/dunder-proto": { "version": "1.0.0", @@ -8358,6 +8366,12 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jspdf/node_modules/dompurify": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", + "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", + "optional": true + }, "node_modules/jsrp": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/jsrp/-/jsrp-0.2.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6e92ba789..29147df1a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,6 +58,7 @@ "classnames": "^2.5.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", + "dompurify": "^3.2.4", "file-saver": "^2.0.5", "framer-motion": "^11.14.1", "i18next": "^24.1.0", diff --git a/frontend/src/components/page-frames/Banner.tsx b/frontend/src/components/page-frames/Banner.tsx new file mode 100644 index 000000000..71c82a87d --- /dev/null +++ b/frontend/src/components/page-frames/Banner.tsx @@ -0,0 +1,17 @@ +/* eslint-disable react/no-danger */ +import DOMPurify from "dompurify"; + +import { useServerConfig } from "@app/context"; + +export const Banner = () => { + const { config } = useServerConfig(); + + // eslint-disable-next-line react/no-danger-with-children + return config.pageFrameContent ? ( +
+
+
+ ) : ( +
+ ); +}; diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 797254893..a45caa184 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -59,7 +59,8 @@ export const leaveConfirmDefaultMessage = export enum SessionStorageKeys { CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN", - ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL" + ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL", + AUTH_CONSENT = "AUTH_CONSENT" } export const secretTagsColors = [ diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 60fa3ab98..e5d281fc4 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -22,6 +22,8 @@ export type TServerConfig = { defaultAuthOrgAuthMethod?: string | null; defaultAuthOrgAuthEnforced?: boolean | null; enabledLoginMethods: LoginMethod[]; + authConsentContent?: string; + pageFrameContent?: string; }; export type TCreateAdminUserDTO = { diff --git a/frontend/src/layouts/AdminLayout/AdminLayout.tsx b/frontend/src/layouts/AdminLayout/AdminLayout.tsx index 84a7adc7c..41e9e6606 100644 --- a/frontend/src/layouts/AdminLayout/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout/AdminLayout.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { WishForm } from "@app/components/features/WishForm"; +import { Banner } from "@app/components/page-frames/Banner"; import { DropdownMenu, DropdownMenuContent, @@ -11,6 +12,7 @@ import { DropdownMenuTrigger } from "@app/components/v2"; import { envConfig } from "@app/config/env"; +import { useServerConfig } from "@app/context"; import { ProjectType } from "@app/hooks/api/workspace/types"; import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner"; @@ -18,10 +20,14 @@ import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/Mini export const AdminLayout = () => { const { t } = useTranslation(); + const { config } = useServerConfig(); + + const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen"; return ( <> -
+ + + ); }; diff --git a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx index 589e82895..7b9a09b9d 100644 --- a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx +++ b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx @@ -6,6 +6,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; +import { Banner } from "@app/components/page-frames/Banner"; import { BreadcrumbContainer, Menu, @@ -13,6 +14,7 @@ import { MenuItem, TBreadcrumbFormat } from "@app/components/v2"; +import { useServerConfig } from "@app/context"; import { usePopUp } from "@app/hooks"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; @@ -22,6 +24,7 @@ import { SidebarHeader } from "./components/SidebarHeader"; export const OrganizationLayout = () => { const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); const location = useLocation(); + const { config } = useServerConfig(); const isOrganizationSpecificPage = location.pathname.startsWith("/organization"); const breadcrumbs = isOrganizationSpecificPage && matches && "breadcrumbs" in matches @@ -45,9 +48,14 @@ export const OrganizationLayout = () => { ] as string[] ).includes(location.pathname); + const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen"; + return ( <> -
+ + + +
+
Notices
+
+ Configure system-wide notification banners and security messages. These + settings control the text displayed to users during authentication and + throughout their session +
+ ( + +