Merge remote-tracking branch 'origin/main' into pki-revamp

This commit is contained in:
Carlos Monastyrski
2025-10-16 12:33:45 -03:00
211 changed files with 3325 additions and 322 deletions
@@ -16,6 +16,7 @@ export const AccessManagementPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="instance"
title="Access Control"
description="Manage server admins within your Infisical instance."
/>
@@ -16,6 +16,7 @@ export const AuthenticationPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="instance"
title="Authentication"
description="Manage authentication settings for your Infisical instance."
/>
@@ -15,7 +15,11 @@ export const CachingPage = () => {
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="Caching" description="Manage caching for your Infisical instance." />
<PageHeader
scope="instance"
title="Caching"
description="Manage caching for your Infisical instance."
/>
<CachingPageForm />
</div>
</div>
@@ -16,6 +16,7 @@ export const EncryptionPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="instance"
title="Encryption"
description="Manage encryption settings for your Infisical instance."
/>
@@ -16,6 +16,7 @@ export const EnvironmentPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="instance"
title="Environment Variables"
description="Manage the environment variables for your Infisical instance."
/>
@@ -18,6 +18,7 @@ export const GeneralPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="instance"
title="General"
description="Manage general settings for your Infisical instance."
/>
@@ -16,6 +16,7 @@ export const IntegrationsPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="instance"
title="Integrations"
description="Manage integrations for your Infisical instance."
/>
@@ -16,6 +16,7 @@ export const ResourceOverviewPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="instance"
title="Resource Overview"
description="Manage resources within your Infisical instance."
/>
@@ -153,7 +153,7 @@ const UserPanelTable = ({
icon={adminsOnly && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<div className="flex items-center gap-x-2">
<FontAwesomeIcon icon={faUserShield} className="text-yellow-700" />
<span>Server Admins</span>
</div>
@@ -16,6 +16,7 @@ export const AlertingPage = () => {
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Alerting"
description="Configure alerts for expiring certificates and CAs to maintain security and compliance."
/>
@@ -87,7 +87,7 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
<PageHeader scope="project" title={data.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -17,6 +17,7 @@ export const CertificateAuthoritiesPage = () => {
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Certificate Authorities"
description="Manage certificate authorities for issuing and signing certificates"
/>
@@ -33,6 +33,7 @@ export const CertificatesPage = () => {
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Certificates"
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
/>
@@ -45,6 +45,7 @@ export const IntegrationsListPage = () => {
<div className="relative container mx-auto max-w-7xl pb-12 text-white">
<div className="mb-8">
<PageHeader
scope="project"
title="Integrations"
description="Manage integrations with third-party certificate services."
/>
@@ -73,7 +73,7 @@ export const PkiCollectionPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
<PageHeader scope="project" title={data.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -78,7 +78,7 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
<PageHeader scope="project" title={data.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -16,6 +16,7 @@ export const PkiSubscribersPage = () => {
<div className="container mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Subscribers"
description="Manage subscribers that request and receive certificates, including user devices, servers, and services."
/>
@@ -107,6 +107,7 @@ export const PkiTemplateListPage = () => {
<div className="container mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Certificate Templates"
description="Manage certificate template to request and issue dynamic certificates following a strict format."
/>
@@ -21,7 +21,7 @@ export const SettingsPage = () => {
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader title={t("settings.project.title")} />
<PageHeader scope="project" title={t("settings.project.title")} />
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (
@@ -18,6 +18,7 @@ export const KmipPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="KMIP"
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
/>
@@ -18,7 +18,8 @@ export const OverviewPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Overview Page"
scope="project"
title="Overview"
description="Manage keys and perform cryptographic operations."
/>
<ProjectPermissionCan
@@ -21,7 +21,7 @@ export const SettingsPage = () => {
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader title="Settings" />
<PageHeader scope="project" title="Settings" />
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (
@@ -82,7 +82,8 @@ export const AccessManagementPage = () => {
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Organization Access Control"
scope="org"
title="Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/>
{!currentOrg.shouldUseNewPrivilegeSystem && (
@@ -115,7 +116,7 @@ export const AccessManagementPage = () => {
{tabSections
.filter((el) => !el.isHidden)
.map((el) => (
<Tab value={el.key} key={`org-access-tab-${el.key}`}>
<Tab variant="org" value={el.key} key={`org-access-tab-${el.key}`}>
{el.label}
</Tab>
))}
@@ -22,6 +22,7 @@ export const AppConnectionsPage = withPermission(
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
scope="org"
className="w-full"
title="App Connections"
description="Manage organization App Connections"
@@ -31,6 +31,7 @@ import { GitLabConnectionForm } from "./GitLabConnectionForm";
import { HCVaultConnectionForm } from "./HCVaultConnectionForm";
import { HerokuConnectionForm } from "./HerokuAppConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { LaravelForgeConnectionForm } from "./LaravelForgeConnectionForm";
import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { MySqlConnectionForm } from "./MySqlConnectionForm";
@@ -146,6 +147,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
return <HerokuConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.Render:
return <RenderConnectionForm onSubmit={onSubmit} />;
case AppConnection.LaravelForge:
return <LaravelForgeConnectionForm onSubmit={onSubmit} />;
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitLab:
@@ -297,6 +300,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
);
case AppConnection.Render:
return <RenderConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.LaravelForge:
return <LaravelForgeConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.GitLab:
@@ -0,0 +1,136 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
LaravelForgeConnectionMethod,
TLaravelForgeConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TLaravelForgeConnection;
onSubmit: (formData: FormData) => Promise<void>;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.LaravelForge)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(LaravelForgeConnectionMethod.ApiToken),
credentials: z.object({
apiToken: z.string().trim().min(1, "API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const LaravelForgeConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.LaravelForge,
method: LaravelForgeConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.LaravelForge].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(LaravelForgeConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Laravel Forge"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -19,7 +19,7 @@ export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
<img
alt={`${appDetails.name} logo`}
src={`/images/integrations/${appDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
className="h-12 w-12 rounded-md bg-bunker-500 object-contain p-2"
/>
{appDetails.icon && (
<FontAwesomeIcon
@@ -80,7 +80,7 @@ export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
{image && (
<img
src={`/images/integrations/${image}`}
style={{
@@ -89,14 +89,16 @@ export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
className="mt-auto"
alt={`${name} logo`}
/>
{icon && (
)}
{icon && (
<div className="relative">
<FontAwesomeIcon
className="absolute -right-1.5 -bottom-1.5 text-primary-700"
size="xl"
icon={icon}
/>
)}
</div>
</div>
)}
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
@@ -16,7 +16,8 @@ export const AuditLogsPage = () => {
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
title="Audit logs"
scope="org"
title="Audit Logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
<LogsSection pageView />
@@ -18,8 +18,9 @@ export const BillingPage = () => {
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6">
<div className="w-full max-w-7xl">
<PageHeader
scope="org"
title={t("billing.title")}
description="View your billing plan, next billing cycle."
/>
@@ -25,7 +25,9 @@ export const BillingTabGroup = withPermission(
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabsFiltered.map((tab) => (
<Tab value={tab.key}>{tab.name}</Tab>
<Tab variant="org" value={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
<TabPanel value={tabs[0].key}>
@@ -83,7 +83,7 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.group.name}>
<PageHeader scope="org" title={data.group.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -75,7 +75,7 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.identity.name} />
<PageHeader scope="org" title={data.identity.name} />
<div className="flex">
<div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
@@ -14,6 +14,7 @@ export const NetworkingPage = () => {
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
scope="org"
title="Networking"
description="Manage gateways and relays to securely access private network resources from Infisical"
/>
@@ -22,7 +22,7 @@ export const NetworkingTabGroup = () => {
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
<Tab variant="org" value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
@@ -65,7 +65,8 @@ export const ProjectsPage = () => {
</Helmet>
<div className="mb-4 flex flex-col items-start justify-start">
<PageHeader
title="Projects"
scope="org"
title="Overview"
description="Your team's complete security toolkit - organized and ready when you need them."
/>
</div>
@@ -81,6 +81,7 @@ export const Page = () => {
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="org"
title={
<div className="flex flex-col">
<div>
@@ -22,6 +22,7 @@ export const SecretSharingPage = () => {
<div className="h-full">
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
<PageHeader
scope="org"
title="Secret Sharing"
description="Share secrets securely using a shareable link"
>
@@ -21,7 +21,7 @@ export const SecretSharingSettingsPage = withPermission(
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="Secret Share Settings" />
<PageHeader scope="org" title="Secret Share Settings" />
<SecretSharingSettingsTabGroup />
</div>
</div>
@@ -15,7 +15,7 @@ export const SettingsPage = () => {
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title={t("settings.org.title")} />
<PageHeader scope="org" title={t("settings.org.title")} />
<OrgTabGroup />
</div>
</div>
@@ -58,7 +58,7 @@ export const OrgTabGroup = () => {
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
<Tab variant="org" value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
@@ -119,6 +119,7 @@ const Page = withPermission(
{membership && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="org"
title={
membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
@@ -23,7 +23,11 @@ export const PamAccountsPage = () => {
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="Accounts" description="View, access, and manage accounts." />
<PageHeader
scope="project"
title="Accounts"
description="View, access, and manage accounts."
/>
<PamAccountsSection />
</div>
</div>
@@ -24,6 +24,7 @@ export const PamResourcesPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Resources"
description="Manage resources such as servers, databases, and more."
/>
@@ -23,6 +23,7 @@ const Page = () => {
{session && (
<div className="mx-auto mb-6 flex w-full max-w-7xl flex-col">
<PageHeader
scope="project"
title={`${session.accountName} Session`}
description={`View details for this ${session.accountName} session.`}
/>
@@ -24,6 +24,7 @@ export const PamSessionPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Sessions"
description="Filter and search through account sessions."
/>
@@ -13,7 +13,7 @@ export const SettingsPage = () => {
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader title="Settings" description="Configure your PAM project." />
<PageHeader scope="project" title="Settings" description="Configure your PAM project." />
<Tabs defaultValue="tab-project-general">
<TabList>
<Tab value="tab-project-general">General</Tab>
@@ -40,6 +40,7 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your project resources."
/>
@@ -23,6 +23,7 @@ export const AppConnectionsPage = withProjectPermission(
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
scope="project"
className="w-full"
title="App Connections"
description="Manage project App Connections"
@@ -16,6 +16,7 @@ export const AuditLogsPage = () => {
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
scope="project"
title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
@@ -34,7 +34,7 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{groupMembership ? (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={groupMembership.group.name} />
<PageHeader scope="project" title={groupMembership.group.name} />
<div className="flex">
<div className="mr-4 w-96">
<GroupDetailsSection groupMembership={groupMembership} />
@@ -117,6 +117,7 @@ const Page = () => {
{identityMembershipDetails ? (
<>
<PageHeader
scope="project"
title={identityMembershipDetails?.identity?.name}
description={`Identity joined on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
>
@@ -119,6 +119,7 @@ export const Page = () => {
{membershipDetails ? (
<>
<PageHeader
scope="project"
title={
membershipDetails.user.firstName || membershipDetails.user.lastName
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
@@ -90,6 +90,7 @@ const Page = () => {
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title={
<div className="flex flex-col">
<div>
@@ -260,6 +260,7 @@ export const CommitDetailsTab = ({
Commit History
</Button>
<PageHeader
scope="project"
title={`${parsedCommitDetails.changes?.message}` || "No message"}
description={
<>
@@ -307,6 +307,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
<div className="h-full w-full">
<div>
<PageHeader
scope="project"
title={`Restore folder at commit ${selectedCommitId.substring(0, 8)}`}
description={`Will return all changes in this folder to how they appeared at the point of commit ${selectedCommitId.substring(0, 8)}. Any modifications made after this commit will be undone.`}
/>
@@ -49,6 +49,7 @@ export const CommitsPage = () => {
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
<div className="w-full max-w-[75vw]">
<PageHeader
scope="project"
title="Commits"
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
/>
@@ -94,6 +94,7 @@ export const IntegrationDetailsByIDPage = () => {
{integration ? (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title={`${integrationSlugNameMapping[integration.integration]} Integration`}
>
<DropdownMenu>
@@ -48,6 +48,7 @@ export const IntegrationsListPage = () => {
<div className="relative container mx-auto max-w-7xl pb-12 text-white">
<div className="mb-8">
<PageHeader
scope="project"
title="Integrations"
description="Manage integrations with third-party services."
/>
@@ -0,0 +1,14 @@
import { TLaravelForgeSync } from "@app/hooks/api/secretSyncs/types/laravel-forge-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TLaravelForgeSync;
};
export const LaravelForgeSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -20,6 +20,7 @@ import { GitLabSyncDestinationCol } from "./GitLabSyncDestinationCol";
import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { LaravelForgeSyncDestinationCol } from "./LaravelForgeSyncDestinationCol";
import { NetlifySyncDestinationCol } from "./NetlifySyncDestinationCol";
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol";
@@ -97,6 +98,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <NetlifySyncDestinationCol secretSync={secretSync} />;
case SecretSync.Bitbucket:
return <BitbucketSyncDestinationCol secretSync={secretSync} />;
case SecretSync.LaravelForge:
return <LaravelForgeSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -194,6 +194,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.workspaceSlug;
secondaryText = destinationConfig.repositorySlug;
break;
case SecretSync.LaravelForge:
primaryText = destinationConfig.siteName || destinationConfig.siteId;
secondaryText = destinationConfig.orgName || destinationConfig.orgSlug;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
@@ -914,6 +914,7 @@ export const OverviewPage = () => {
<div className="relative mx-auto max-w-7xl text-mineshaft-50 dark:scheme-dark">
<div className="flex w-full items-baseline justify-between">
<PageHeader
scope="project"
title="Secrets Overview"
description={
<p className="text-md text-bunker-300">
@@ -82,6 +82,7 @@ type Props = {
isImported: boolean;
}[];
}[];
isSecretPresent?: boolean;
};
export const SecretEditRow = ({
@@ -101,7 +102,8 @@ export const SecretEditRow = ({
isRotatedSecret,
importedBy,
importedSecret,
isEmpty
isEmpty,
isSecretPresent
}: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"editSecret"
@@ -113,20 +115,21 @@ export const SecretEditRow = ({
const [isFieldFocused, setIsFieldFocused] = useToggle();
const fetchSecretValueParams = importedSecret
? {
environment: importedSecret.environment,
secretPath: importedSecret.secretPath,
secretKey: importedSecret.secret?.key ?? "",
projectId: currentProject.id
}
: {
environment,
secretPath,
secretKey: secretName,
projectId: currentProject.id,
isOverride
};
const fetchSecretValueParams =
importedSecret && !isSecretPresent
? {
environment: importedSecret.environment,
secretPath: importedSecret.secretPath,
secretKey: importedSecret.secret?.key ?? "",
projectId: currentProject.id
}
: {
environment,
secretPath,
secretKey: secretName,
projectId: currentProject.id,
isOverride
};
// scott: only fetch value if secret exists, has non-empty value and user has permission
const canFetchValue = Boolean(importedSecret ?? secretId) && !isEmpty && !secretValueHidden;
@@ -284,6 +284,7 @@ export const SecretOverviewTableRow = ({
environment={slug}
isRotatedSecret={secret?.isRotatedSecret}
importedBy={importedBy}
isSecretPresent={Boolean(secret)}
/>
</td>
</tr>
@@ -40,6 +40,7 @@ export const SecretApprovalsPage = () => {
</Helmet>
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
<PageHeader
scope="project"
title="Approval Workflows"
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
/>
@@ -252,7 +252,7 @@ export const SecretApprovalRequestChanges = ({ approvalRequestId, onGoBack }: Pr
secretApprovalRequestDetails.isReplicated
)}
</div>
<p className="-mt-1 truncate text-xs text-gray-400">
<p className="-mt-1 truncate text-sm text-gray-400">
By{" "}
{secretApprovalRequestDetails?.committerUser ? (
<>
@@ -762,6 +762,7 @@ const Page = () => {
return (
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:scheme-dark">
<PageHeader
scope="project"
title="Secrets Management"
description={
<p className="text-md text-bunker-300">
@@ -380,7 +380,7 @@ export const ActionBar = ({
}
};
// Replicate Folder Logic
// Replicate Secrets Logic
const createSecretCount = Object.keys(
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
).length;
@@ -439,17 +439,22 @@ export const ActionBar = ({
const processBatches = async () => {
await secretBatches.reduce(async (previous, batch) => {
await previous;
try {
const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({
secretPath: normalizedPath,
environment,
projectId,
keys: batch
});
const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({
secretPath: normalizedPath,
environment,
projectId,
keys: batch
});
batchSecrets.forEach((secret) => {
existingSecretLookup.add(`${normalizedPath}-${secret.secretKey}`);
});
batchSecrets.forEach((secret) => {
existingSecretLookup.add(`${normalizedPath}-${secret.secretKey}`);
});
} catch (error) {
if (!(error instanceof AxiosError && error.response?.status === 404)) {
throw error;
}
}
}, Promise.resolve());
};
@@ -1050,7 +1055,7 @@ export const ActionBar = ({
className="h-10 text-left"
isFullWidth
>
Replicate Folder
Replicate Secrets
</Button>
)}
</ProjectPermissionCan>
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faClone } from "@fortawesome/free-solid-svg-icons";
import { faArrowUpRightFromSquare, faBookOpen, faClone } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -226,8 +226,27 @@ export const ReplicateFolderFromBoard = ({
<ModalContent
bodyClassName="overflow-visible"
className="max-w-2xl"
title="Replicate Folder Content From An Environment"
subTitle="Replicate folder content from other environments into this context"
title={
<div className="flex items-center gap-1 text-mineshaft-300">
<span>Replicate Secrets</span>
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/folder#replicating-folder-contents"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
}
subTitle="Copy folder contents from other locations into this context"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="flex items-center space-x-2">
@@ -235,7 +254,12 @@ export const ReplicateFolderFromBoard = ({
control={control}
name="environment"
render={({ field: { value, onChange } }) => (
<FormControl label="Environment" isRequired className="w-1/3">
<FormControl
tooltipText="The environment to replicate secrets from"
label="Source Environment"
isRequired
className="w-1/3"
>
<FilterableSelect
value={value}
onChange={onChange}
@@ -251,7 +275,12 @@ export const ReplicateFolderFromBoard = ({
control={control}
name="secretPath"
render={({ field }) => (
<FormControl label="Secret Path" className="grow" isRequired>
<FormControl
tooltipText="The folder to use as a root for replication. Using /foo as a root for /foo/bar will result in /bar being copied to this context."
label="Source Root Path"
className="grow"
isRequired
>
<SecretPathInput
{...field}
placeholder="Provide a path, default is /"
@@ -261,44 +290,49 @@ export const ReplicateFolderFromBoard = ({
)}
/>
</div>
<div className="border-t border-mineshaft-600 pt-4">
<Controller
control={control}
name="secrets"
render={({ field: { onChange } }) => (
<FormControl className="grow" isRequired>
<SecretTreeView
data={secretsFilteredByPath}
basePath={debouncedEnvCopySecretPath}
onChange={onChange}
/>
</FormControl>
)}
/>
<div className="my-6 ml-2">
<Switch
id="populate-include-value"
isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => {
setValue("secrets", []);
setShouldIncludeValues(isChecked as boolean);
}}
<Controller
control={control}
name="secrets"
render={({ field: { onChange } }) => (
<FormControl
tooltipText="The folders and secrets to replicate into this context"
className="grow"
label="Affected Subjects"
isRequired
>
Include secret values
</Switch>
</div>
<div className="flex items-center space-x-4">
<Button
leftIcon={<FontAwesomeIcon icon={faClone} />}
type="submit"
isDisabled={!selectedSecrets || selectedSecrets.length === 0}
>
Replicate Folder
</Button>
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}>
Cancel
</Button>
</div>
<SecretTreeView
data={secretsFilteredByPath}
basePath={debouncedEnvCopySecretPath}
onChange={onChange}
/>
</FormControl>
)}
/>
<div className="my-6 ml-2">
<Switch
id="populate-include-value"
isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => {
setValue("secrets", []);
setShouldIncludeValues(isChecked as boolean);
}}
>
Include secret values
</Switch>
</div>
<div className="flex items-center space-x-4">
<Button
leftIcon={<FontAwesomeIcon icon={faClone} />}
type="submit"
variant="outline_bg"
colorSchema="secondary"
isDisabled={!selectedSecrets || selectedSecrets.length === 0}
>
Replicate Secrets
</Button>
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}>
Cancel
</Button>
</div>
</form>
</ModalContent>
@@ -33,11 +33,15 @@ type TImportedSecrets = Array<{
}>;
export const computeImportedSecretRows = (
importedSecEnv: string,
importedSecPath: string,
secretImport: TSecretImport,
importSecrets: TImportedSecrets = [],
secrets: SecretV3RawSanitized[] = []
secrets: SecretV3RawSanitized[] = [],
replicatedFolder?: TSecretImport
) => {
const importedSecEnv = replicatedFolder?.importEnv.slug ?? secretImport.importEnv.slug;
const importedSecPath = replicatedFolder?.importPath ?? secretImport.importPath;
const overrideEnv = secretImport.isReserved ? secretImport.importEnv.slug : undefined;
const overridePath = secretImport.isReserved ? secretImport.importPath : undefined;
const importedSecIndex = importSecrets.findIndex(
({ secretPath, environmentInfo }) =>
secretPath === importedSecPath && importedSecEnv === environmentInfo.slug
@@ -73,13 +77,13 @@ export const computeImportedSecretRows = (
isEmpty?: boolean;
}[] = [];
importedSec.secrets.forEach(({ key, value, env, path, isEmpty }) => {
importedSec.secrets.forEach(({ key, value, isEmpty }) => {
if (!importedEntry[key]) {
importedSecretEntries.push({
key,
value,
environment: env,
secretPath: path,
environment: overrideEnv ?? importedSec.environmentInfo.slug,
secretPath: overridePath ?? importedSec.secretPath,
overridden: overridenSec?.[key],
isEmpty
});
@@ -125,6 +129,12 @@ export const SecretImportListView = ({
const [items, setItems] = useState(secretImports ?? []);
const getImportReplicatedFolder = (importPath: string) => {
const cleanImportPath = importPath.replace("/__reserve_replication_", "");
const replicatedFolder = items?.find(({ id }) => id === cleanImportPath);
return replicatedFolder;
};
useEffect(() => {
if (!isFetching) {
setItems(secretImports ?? []);
@@ -204,6 +214,9 @@ export const SecretImportListView = ({
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items?.map((item) => {
// TODO(akhilmhdh): change this and pass this whole object instead of one by one
const replicatedFolder = item.isReserved
? getImportReplicatedFolder(item.importPath)
: undefined;
return (
<SecretImportItem
searchTerm={searchTerm}
@@ -212,10 +225,11 @@ export const SecretImportListView = ({
onExpandReplicateSecrets={handleOpenReplicationSecrets}
secretImport={item}
importedSecrets={computeImportedSecretRows(
item.importEnv.slug,
item.importPath,
importedSecrets
item,
importedSecrets,
[],
// secrets scott - now that secrets are paginated we are not showing if they are overridden (yet?)
replicatedFolder
)}
secretPath={secretPath}
environment={environment}
@@ -79,7 +79,7 @@ type Props = {
isSelected?: boolean;
onToggleSecretSelect: (secret: SecretV3RawSanitized) => void;
tags: WsTag[];
onCreateTag: () => void;
onCreateTag: (secret?: SecretV3RawSanitized) => void;
environment: string;
secretPath: string;
onShareSecret: (sec: SecretV3RawSanitized) => void;
@@ -731,7 +731,7 @@ export const SecretItem = memo(
className="h-3 w-3"
/>
}
onClick={onCreateTag}
onClick={() => onCreateTag(secret)}
>
Create a tag
</Button>
@@ -924,7 +924,7 @@ export const SecretItem = memo(
className="h-3 w-3"
/>
}
onClick={onCreateTag}
onClick={() => onCreateTag(secret)}
>
Create a tag
</Button>
@@ -465,6 +465,22 @@ export const SecretListView = ({
[environment, secretPath, isProtectedBranch, isBatchMode, projectId, addPendingChange]
);
// Function to append newly created tag to the current secret
const append = useCallback(
(newTag: WsTag) => {
const currentSecret = popUp.createTag.data as SecretV3RawSanitized;
if (!currentSecret) return;
const updatedTags = [...(currentSecret.tags || []), { id: newTag.id, slug: newTag.slug }];
handleSaveSecret(currentSecret, {
...currentSecret,
tags: updatedTags
});
},
[popUp.createTag.data, handleSaveSecret]
);
const handleSecretDelete = useCallback(async () => {
const {
key,
@@ -552,7 +568,13 @@ export const SecretListView = ({
]);
// for optimization on minimise re-rendering of secret items
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);
const onCreateTag = useCallback((secret?: SecretV3RawSanitized) => {
if (secret) {
handlePopUpOpen("createTag", secret);
} else {
handlePopUpOpen("createTag");
}
}, []);
const onDeleteSecret = useCallback(
(sec: SecretV3RawSanitized) => handlePopUpOpen("deleteSecret", sec),
[]
@@ -640,6 +662,8 @@ export const SecretListView = ({
<CreateTagModal
isOpen={popUp.createTag.isOpen}
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
append={append}
currentSecret={popUp.createTag.data}
/>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</>
@@ -147,6 +147,7 @@ const Page = () => {
return (
<div className="container mx-auto w-full max-w-7xl bg-bunker-800 text-white">
<PageHeader
scope="project"
title="Secret Rotation"
description="Stop manually rotating secrets and automate credential rotation."
>
@@ -0,0 +1,24 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TLaravelForgeSync } from "@app/hooks/api/secretSyncs/types/laravel-forge-sync";
type Props = {
secretSync: TLaravelForgeSync;
};
export const LaravelForgeSyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
return (
<>
<GenericFieldLabel label="Account">
{destinationConfig.orgName || destinationConfig.orgSlug}
</GenericFieldLabel>
<GenericFieldLabel label="Server">
{destinationConfig.serverName || destinationConfig.serverId}
</GenericFieldLabel>
<GenericFieldLabel label="Site">
{destinationConfig.siteName || destinationConfig.siteId}
</GenericFieldLabel>
</>
);
};
@@ -31,6 +31,7 @@ import { GitLabSyncDestinationSection } from "./GitLabSyncDestinationSection";
import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { LaravelForgeSyncDestinationSection } from "./LaravelForgeSyncDestinationSection";
import { NetlifySyncDestinationSection } from "./NetlifySyncDestinationSection";
import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection";
import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection";
@@ -148,6 +149,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Bitbucket:
DestinationComponents = <BitbucketSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.LaravelForge:
DestinationComponents = <LaravelForgeSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
@@ -71,6 +71,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.DigitalOceanAppPlatform:
case SecretSync.Netlify:
case SecretSync.Bitbucket:
case SecretSync.LaravelForge:
AdditionalSyncOptionsComponent = null;
break;
default:
@@ -42,6 +42,7 @@ export const SettingsPage = () => {
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader
scope="project"
title="Settings"
description="Configure your secret manager's encryption, environments, webhooks and other configurations."
/>
@@ -26,6 +26,7 @@ export const SecretScanningDataSourcesPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Data Sources"
description="Manage your Secret Scanning data sources."
/>
@@ -23,7 +23,11 @@ export const SecretScanningFindingsPage = () => {
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="Findings" description="View Secret Leaks across your project." />
<PageHeader
scope="project"
title="Findings"
description="View Secret Leaks across your project."
/>
<SecretScanningFindingsSection />
</div>
</div>
@@ -19,6 +19,7 @@ export const SettingsPage = () => {
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader
scope="project"
title="Settings"
description="Configure your Secret Scanning product's configurations."
/>
@@ -17,7 +17,11 @@ export const SettingsPage = () => {
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader title="Settings" description="Configure your SSH product's configurations." />
<PageHeader
scope="project"
title="Settings"
description="Configure your SSH product's configurations."
/>
<Tabs defaultValue="tab-project-general">
<TabList>
<Tab value="tab-project-general">General</Tab>
@@ -70,7 +70,7 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.friendlyName}>
<PageHeader scope="project" title={data.friendlyName}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -16,6 +16,7 @@ export const SshCasPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="SSH Certificate Authorities"
description="Manage the SSH certificate authorities used to sign user and host certificates, including custom and default CAs."
/>
@@ -16,6 +16,7 @@ export const SshCertsPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="SSH Certificates"
description="View and audit all issued SSH certificates, including validity and associated access metadata."
/>
@@ -71,7 +71,7 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
<PageHeader scope="project" title={data.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -16,6 +16,7 @@ export const SshHostsPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Hosts"
description="Manage your SSH hosts, configure access policies, and define login behavior for secure connections."
/>