Merge pull request #3154 from Infisical/misc/gov-banner-and-consent-reqs

misc: add instance banner and consent support
This commit is contained in:
Sheen
2025-03-04 01:46:54 +08:00
committed by GitHub
18 changed files with 2067 additions and 69 deletions
@@ -0,0 +1,20 @@
import ReactMarkdown from "react-markdown";
import DOMPurify from "dompurify";
import rehypeRaw from "rehype-raw";
import { useServerConfig } from "@app/context";
export const Banner = () => {
const { config } = useServerConfig();
// eslint-disable-next-line react/no-danger-with-children
return config.pageFrameContent ? (
<div className="h-[3vh] w-full text-center">
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
{DOMPurify.sanitize(config.pageFrameContent)}
</ReactMarkdown>
</div>
) : (
<div />
);
};
+2 -1
View File
@@ -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 = [
+2
View File
@@ -22,6 +22,8 @@ export type TServerConfig = {
defaultAuthOrgAuthMethod?: string | null;
defaultAuthOrgAuthEnforced?: boolean | null;
enabledLoginMethods: LoginMethod[];
authConsentContent?: string;
pageFrameContent?: string;
};
export type TCreateAdminUserDTO = {
@@ -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 (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
<Banner />
<div className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden md:flex`}>
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
@@ -69,7 +75,6 @@ export const AdminLayout = () => {
</DropdownMenuContent>
</DropdownMenu>
</div>
)
</nav>
</aside>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
@@ -83,6 +88,7 @@ export const AdminLayout = () => {
{` ${t("common.no-mobile")} `}
</p>
</div>
<Banner />
</>
);
};
@@ -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 (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex">
<Banner />
<div
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
>
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<MinimizedOrgSidebar />
@@ -144,6 +152,7 @@ export const OrganizationLayout = () => {
{` ${t("common.no-mobile")} `}
</p>
</div>
<Banner />
</>
);
};
@@ -19,7 +19,8 @@ import {
Tab,
TabList,
TabPanel,
Tabs
Tabs,
TextArea
} from "@app/components/v2";
import { useServerConfig, useUser } from "@app/context";
import {
@@ -55,7 +56,9 @@ const formSchema = z.object({
trustSamlEmails: z.boolean(),
trustLdapEmails: z.boolean(),
trustOidcEmails: z.boolean(),
defaultAuthOrgId: z.string()
defaultAuthOrgId: z.string(),
authConsentContent: z.string().optional(),
pageFrameContent: z.string().optional()
});
type TDashboardForm = z.infer<typeof formSchema>;
@@ -80,7 +83,9 @@ export const OverviewPage = () => {
trustSamlEmails: config.trustSamlEmails,
trustLdapEmails: config.trustLdapEmails,
trustOidcEmails: config.trustOidcEmails,
defaultAuthOrgId: config.defaultAuthOrgId ?? ""
defaultAuthOrgId: config.defaultAuthOrgId ?? "",
authConsentContent: config.authConsentContent,
pageFrameContent: config.pageFrameContent
}
});
@@ -95,7 +100,14 @@ export const OverviewPage = () => {
const isNotAllowed = !user?.superAdmin;
const onFormSubmit = async (formData: TDashboardForm) => {
try {
const { allowedSignUpDomain, trustSamlEmails, trustLdapEmails, trustOidcEmails } = formData;
const {
allowedSignUpDomain,
trustSamlEmails,
trustLdapEmails,
trustOidcEmails,
authConsentContent,
pageFrameContent
} = formData;
await updateServerConfig({
defaultAuthOrgId: defaultAuthOrgId || null,
@@ -103,7 +115,9 @@ export const OverviewPage = () => {
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
trustSamlEmails,
trustLdapEmails,
trustOidcEmails
trustOidcEmails,
authConsentContent,
pageFrameContent
});
createNotification({
text: "Successfully changed sign up setting.",
@@ -324,6 +338,51 @@ export const OverviewPage = () => {
}}
/>
</div>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Notices</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-400">
Configure system-wide notification banners and security messages. These
settings control the text displayed to users during authentication and
throughout their session
</div>
<Controller
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Auth Consent Content"
>
<TextArea
placeholder="Auth Consent Message"
{...field}
rows={3}
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="authConsentContent"
/>
<Controller
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Page Frame Content"
>
<TextArea
placeholder="Page Frame Content"
{...field}
rows={3}
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="pageFrameContent"
/>
</div>
<Button
type="submit"
isLoading={isSubmitting}
@@ -1,7 +1,15 @@
import { createFileRoute, redirect, stripSearchParams } from "@tanstack/react-router";
import { useState } from "react";
import ReactMarkdown from "react-markdown";
import { createFileRoute, Outlet, redirect, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { addSeconds, formatISO } from "date-fns";
import DOMPurify from "dompurify";
import rehypeRaw from "rehype-raw";
import { z } from "zod";
import { Button } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useServerConfig } from "@app/context";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { setAuthToken } from "@app/hooks/api/reactQuery";
import { ProjectType } from "@app/hooks/api/workspace/types";
@@ -10,6 +18,56 @@ const QueryParamsSchema = z.object({
callback_port: z.coerce.number().optional().catch(undefined)
});
export const AuthConsentWrapper = () => {
const { config } = useServerConfig();
const [hasConsented, setHasConsented] = useState(() => {
const consentInfo = sessionStorage.getItem(SessionStorageKeys.AUTH_CONSENT);
if (!consentInfo) {
return false;
}
const { expiry, data } = JSON.parse(consentInfo);
if (new Date() > new Date(expiry)) {
sessionStorage.removeItem(SessionStorageKeys.AUTH_CONSENT);
return false;
}
return data === "true";
});
const handleConsent = () => {
sessionStorage.setItem(
SessionStorageKeys.AUTH_CONSENT,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 60)),
data: "true"
})
);
setHasConsented(true);
};
return (
<>
{config.authConsentContent && !hasConsented && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-mineshaft-700/80 bg-opacity-90">
<div className="max-h-[80vh] w-4/12 overflow-y-auto rounded-lg bg-bunker-800 p-6 text-white">
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
{DOMPurify.sanitize(config.authConsentContent)}
</ReactMarkdown>
<div className="mt-6 flex justify-end">
<Button onClick={handleConsent} colorSchema="secondary">
OK
</Button>
</div>
</div>
</div>
)}
<Outlet />
</>
);
};
export const Route = createFileRoute("/_restrict-login-signup")({
validateSearch: zodValidator(QueryParamsSchema),
search: {
@@ -45,5 +103,6 @@ export const Route = createFileRoute("/_restrict-login-signup")({
throw redirect({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
}
},
component: AuthConsentWrapper
});