mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 13:27:22 +00:00
Merge branch 'main' into ENG-3506-LDAP
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@@ -1,6 +1,7 @@
|
||||
import { FunctionComponent, ReactNode } from "react";
|
||||
import { BoundCanProps, Can } from "@casl/react";
|
||||
|
||||
import { TooltipProps } from "@app/components/v2/Tooltip/Tooltip";
|
||||
import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
|
||||
|
||||
import { AccessRestrictedBanner, Tooltip } from "../v2";
|
||||
@@ -20,6 +21,7 @@ type Props = {
|
||||
renderTooltip?: boolean;
|
||||
allowedLabel?: string;
|
||||
renderGuardBanner?: boolean;
|
||||
tooltipProps?: Omit<TooltipProps, "children">;
|
||||
} & BoundCanProps<TOrgPermission>;
|
||||
|
||||
export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||
@@ -29,6 +31,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||
renderTooltip,
|
||||
allowedLabel,
|
||||
renderGuardBanner,
|
||||
tooltipProps,
|
||||
...props
|
||||
}) => {
|
||||
const { permission } = useOrgPermission();
|
||||
@@ -43,11 +46,19 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||
: children;
|
||||
|
||||
if (!isAllowed && passThrough) {
|
||||
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||
return (
|
||||
<Tooltip content={label} {...tooltipProps}>
|
||||
{finalChild}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAllowed && renderTooltip && allowedLabel) {
|
||||
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>;
|
||||
return (
|
||||
<Tooltip content={allowedLabel} {...tooltipProps}>
|
||||
{finalChild}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAllowed && renderGuardBanner) {
|
||||
|
||||
@@ -34,7 +34,9 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
|
||||
|
||||
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
|
||||
// when break is added a line break works properly
|
||||
return formattedContent.concat(<br key={`secret-value-${formattedContent.length + 1}`} />);
|
||||
return formattedContent.concat(
|
||||
<br key={`secret-value-linebreak-${formattedContent.length + 1}`} />
|
||||
);
|
||||
};
|
||||
|
||||
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouteContext } from "@tanstack/react-router";
|
||||
|
||||
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
|
||||
|
||||
export const useSubscription = () => {
|
||||
export const useSubscription = (refreshCache?: boolean) => {
|
||||
const organizationId = useRouteContext({
|
||||
from: "/_authenticate/_inject-org-details",
|
||||
select: (el) => el.organizationId
|
||||
@@ -11,7 +11,7 @@ export const useSubscription = () => {
|
||||
|
||||
const { data: subscription } = useSuspenseQuery({
|
||||
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId),
|
||||
queryFn: () => fetchOrgSubscription(organizationId),
|
||||
queryFn: () => fetchOrgSubscription(organizationId, refreshCache),
|
||||
staleTime: Infinity
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ export const AUDIT_LOG_STREAM_PROVIDER_MAP: Record<
|
||||
LogProvider,
|
||||
{ name: string; image?: string; icon?: IconDefinition; size?: number }
|
||||
> = {
|
||||
[LogProvider.Azure]: { name: "Azure", image: "Microsoft Azure.png", size: 60 },
|
||||
[LogProvider.Cribl]: { name: "Cribl", image: "Cribl.png", size: 60 },
|
||||
[LogProvider.Custom]: { name: "Custom", icon: faCode },
|
||||
[LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" },
|
||||
[LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 }
|
||||
@@ -19,11 +21,13 @@ export function getProviderUrl(
|
||||
) {
|
||||
switch (logStream.provider) {
|
||||
case LogProvider.Custom:
|
||||
return logStream.credentials.url;
|
||||
case LogProvider.Datadog:
|
||||
case LogProvider.Cribl:
|
||||
return logStream.credentials.url;
|
||||
case LogProvider.Splunk:
|
||||
return `https://${logStream.credentials.hostname}:8088/services/collector/event`;
|
||||
case LogProvider.Azure:
|
||||
return `${logStream.credentials.dceUrl}/dataCollectionRules/${logStream.credentials.dcrId}/streams/Custom-${logStream.credentials.cltName}_CL`;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled provider in getProviderUrl: ${(logStream as TAuditLogStream).provider}`
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export enum LogProvider {
|
||||
Azure = "azure",
|
||||
Cribl = "cribl",
|
||||
Custom = "custom",
|
||||
Datadog = "datadog",
|
||||
Splunk = "splunk",
|
||||
Custom = "custom"
|
||||
Splunk = "splunk"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { LogProvider } from "../enums";
|
||||
import { TAzureProviderLogStream } from "./providers/azure-provider";
|
||||
import { TCriblProviderLogStream } from "./providers/cribl-provider";
|
||||
import { TCustomProviderLogStream } from "./providers/custom-provider";
|
||||
import { TDatadogProviderLogStream } from "./providers/datadog-provider";
|
||||
import { TSplunkProviderLogStream } from "./providers/splunk-provider";
|
||||
@@ -6,9 +8,13 @@ import { TSplunkProviderLogStream } from "./providers/splunk-provider";
|
||||
export type TAuditLogStream =
|
||||
| TCustomProviderLogStream
|
||||
| TDatadogProviderLogStream
|
||||
| TSplunkProviderLogStream;
|
||||
| TSplunkProviderLogStream
|
||||
| TAzureProviderLogStream
|
||||
| TCriblProviderLogStream;
|
||||
|
||||
export type TAuditLogStreamProviderMap = {
|
||||
[LogProvider.Azure]: TAzureProviderLogStream;
|
||||
[LogProvider.Cribl]: TCriblProviderLogStream;
|
||||
[LogProvider.Custom]: TCustomProviderLogStream;
|
||||
[LogProvider.Datadog]: TDatadogProviderLogStream;
|
||||
[LogProvider.Splunk]: TSplunkProviderLogStream;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { LogProvider } from "../../enums";
|
||||
import { TRootProviderLogStream } from "./root-provider";
|
||||
|
||||
export type TAzureProviderLogStream = TRootProviderLogStream & {
|
||||
provider: LogProvider.Azure;
|
||||
credentials: {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
dceUrl: string;
|
||||
dcrId: string;
|
||||
cltName: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { LogProvider } from "../../enums";
|
||||
import { TRootProviderLogStream } from "./root-provider";
|
||||
|
||||
export type TCriblProviderLogStream = TRootProviderLogStream & {
|
||||
provider: LogProvider.Cribl;
|
||||
credentials: {
|
||||
url: string;
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./mutations";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { gatewaysQueryKeys } from "../gateways/queries";
|
||||
|
||||
export const useDeleteGatewayV2ById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
return apiRequest.delete(`/api/v2/gateways/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(gatewaysQueryKeys.list());
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export type TGatewayV2 = {
|
||||
id: string;
|
||||
identityId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
heartbeat: string;
|
||||
identity: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { queryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGatewayV2 } from "../gateways-v2/types";
|
||||
import { TGateway } from "./types";
|
||||
|
||||
export const gatewaysQueryKeys = {
|
||||
@@ -11,8 +12,21 @@ export const gatewaysQueryKeys = {
|
||||
queryOptions({
|
||||
queryKey: gatewaysQueryKeys.listKey(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways");
|
||||
return data.gateways;
|
||||
const [{ data }, { data: dataV2 }] = await Promise.all([
|
||||
apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways"),
|
||||
apiRequest.get<TGatewayV2[]>("/api/v2/gateways")
|
||||
]);
|
||||
|
||||
return [
|
||||
...data.gateways.map((g) => ({
|
||||
...g,
|
||||
isV1: true
|
||||
})),
|
||||
...dataV2.map((g) => ({
|
||||
...g,
|
||||
isV1: false
|
||||
}))
|
||||
];
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -10,9 +10,9 @@ export const subscriptionQueryKeys = {
|
||||
getOrgSubsription: (orgID: string) => ["plan", { orgID }] as const
|
||||
};
|
||||
|
||||
export const fetchOrgSubscription = async (orgID: string) => {
|
||||
export const fetchOrgSubscription = async (orgID: string, refreshCache: boolean = false) => {
|
||||
const { data } = await apiRequest.get<{ plan: SubscriptionPlan }>(
|
||||
`/api/v1/organizations/${orgID}/plan`
|
||||
`/api/v1/organizations/${orgID}/plan${refreshCache ? "?refreshCache=true" : ""}`
|
||||
);
|
||||
|
||||
return data.plan;
|
||||
|
||||
@@ -54,5 +54,7 @@ export type SubscriptionPlan = {
|
||||
secretScanning: boolean;
|
||||
enterpriseSecretSyncs: boolean;
|
||||
enterpriseAppConnections: boolean;
|
||||
cardDeclined?: boolean;
|
||||
cardDeclinedReason?: string;
|
||||
machineIdentityAuthTemplates: boolean;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export {
|
||||
useAddUserToWsNonE2EE,
|
||||
useRemoveMyDuplicateAccounts,
|
||||
useRequestEmailChangeOTP,
|
||||
useRevokeMySessionById,
|
||||
useSendEmailVerificationCode,
|
||||
useUpdateUserEmail,
|
||||
useVerifyEmailVerificationCode
|
||||
} from "./mutation";
|
||||
export {
|
||||
|
||||
@@ -152,3 +152,30 @@ export const useRemoveMyDuplicateAccounts = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRequestEmailChangeOTP = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ newEmail }: { newEmail: string }) => {
|
||||
const { data } = await apiRequest.post("/api/v2/users/me/email-change/otp", {
|
||||
newEmail
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserEmail = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ newEmail, otpCode }: { newEmail: string; otpCode: string }) => {
|
||||
const { data } = await apiRequest.patch("/api/v2/users/me/email", {
|
||||
newEmail,
|
||||
otpCode
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.getUser });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faEnvelope,
|
||||
faExclamationTriangle,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faServer,
|
||||
@@ -111,6 +112,14 @@ export const Navbar = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [showAdminsModal, setShowAdminsModal] = useState(false);
|
||||
const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) {
|
||||
sessionStorage.setItem("paymentFailed", "true");
|
||||
setShowCardDeclinedModal(true);
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
const { data: orgs } = useGetOrganizations();
|
||||
const navigate = useNavigate();
|
||||
@@ -222,6 +231,19 @@ export const Navbar = () => {
|
||||
<div className="mr-1 rounded border border-mineshaft-500 px-1 text-xs text-bunker-300 !no-underline">
|
||||
{getPlan(subscription)}
|
||||
</div>
|
||||
{subscription.cardDeclined && (
|
||||
<Tooltip
|
||||
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faExclamationTriangle}
|
||||
className="animate-pulse cursor-help text-xs text-primary-400"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -428,6 +450,49 @@ export const Navbar = () => {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Modal isOpen={showCardDeclinedModal} onOpenChange={setShowCardDeclinedModal}>
|
||||
<ModalContent
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="text-lg text-primary-400" />
|
||||
Your payment could not be processed.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div className="mb-1">
|
||||
<p>
|
||||
We were unable to process your last payment
|
||||
{subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}.
|
||||
Please update your payment information to continue using premium features.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="flex space-x-3">
|
||||
<Link to="/organization/billing" className="inline-flex">
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={() => setShowCardDeclinedModal(false)}
|
||||
>
|
||||
Update Payment Method
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline"
|
||||
className="ml-2"
|
||||
onClick={() => setShowCardDeclinedModal(false)}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
|
||||
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
||||
<div className="mb-2">
|
||||
|
||||
+11
-1
@@ -1,5 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
@@ -15,13 +17,15 @@ import {
|
||||
useGetOrgPlanBillingInfo,
|
||||
useGetOrgTrialUrl
|
||||
} from "@app/hooks/api";
|
||||
import { subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { ManagePlansModal } from "./ManagePlansModal";
|
||||
|
||||
export const PreviewSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { subscription } = useSubscription(true);
|
||||
const queryClient = useQueryClient();
|
||||
const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? "");
|
||||
const getOrgTrialUrl = useGetOrgTrialUrl();
|
||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||
@@ -37,6 +41,12 @@ export const PreviewSection = () => {
|
||||
return formattedTotal;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: subscriptionQueryKeys.getOrgSubsription(currentOrg?.id ?? "")
|
||||
});
|
||||
}, []);
|
||||
|
||||
const formatDate = (date: number) => {
|
||||
const createdDate = new Date(date * 1000);
|
||||
const day: number = createdDate.getDate();
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format, formatRelative } from "date-fns";
|
||||
import { formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
@@ -48,13 +48,14 @@ import {
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
|
||||
import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2";
|
||||
|
||||
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
|
||||
|
||||
export const GatewayListPage = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"deleteGateway",
|
||||
@@ -62,16 +63,20 @@ export const GatewayListPage = withPermission(
|
||||
] as const);
|
||||
|
||||
const deleteGatewayById = useDeleteGatewayById();
|
||||
const deleteGatewayV2ById = useDeleteGatewayV2ById();
|
||||
|
||||
const handleDeleteGateway = async () => {
|
||||
await deleteGatewayById.mutateAsync((popUp.deleteGateway.data as { id: string }).id, {
|
||||
onSuccess: () => {
|
||||
handlePopUpToggle("deleteGateway");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully delete gateway"
|
||||
});
|
||||
}
|
||||
const data = popUp.deleteGateway.data as { id: string; isV1: boolean };
|
||||
if (data.isV1) {
|
||||
await deleteGatewayById.mutateAsync(data.id);
|
||||
} else {
|
||||
await deleteGatewayV2ById.mutateAsync(data.id);
|
||||
}
|
||||
|
||||
handlePopUpToggle("deleteGateway");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted gateway"
|
||||
});
|
||||
};
|
||||
|
||||
@@ -127,7 +132,6 @@ export const GatewayListPage = withPermission(
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Name</Th>
|
||||
<Th>Cert Issued At</Th>
|
||||
<Th>Identity</Th>
|
||||
<Th>
|
||||
Health Check
|
||||
@@ -143,13 +147,19 @@ export const GatewayListPage = withPermission(
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isGatewayLoading && (
|
||||
{isGatewaysLoading && (
|
||||
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
|
||||
)}
|
||||
{filteredGateway?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>{el.name}</Td>
|
||||
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{el.name}</span>
|
||||
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
|
||||
Gateway v{el.isV1 ? "1" : "2"}
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{el.identity.name}</Td>
|
||||
<Td>
|
||||
{el.heartbeat
|
||||
@@ -176,20 +186,22 @@ export const GatewayListPage = withPermission(
|
||||
>
|
||||
Copy ID
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.EditGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => handlePopUpOpen("editDetails", el)}
|
||||
>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
{el.isV1 && (
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.EditGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => handlePopUpOpen("editDetails", el)}
|
||||
>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionAppConnectionActions.Delete}
|
||||
a={OrgPermissionSubjects.AppConnections}
|
||||
@@ -224,7 +236,7 @@ export const GatewayListPage = withPermission(
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isGatewayLoading && !filteredGateway?.length && (
|
||||
{!isGatewaysLoading && !filteredGateway?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
gateways?.length
|
||||
@@ -251,8 +263,5 @@ export const GatewayListPage = withPermission(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionAppConnectionActions.Read,
|
||||
subject: OrgPermissionSubjects.AppConnections
|
||||
}
|
||||
{ action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway }
|
||||
);
|
||||
|
||||
+14
@@ -6,6 +6,8 @@ import { TAuditLogStream } from "@app/hooks/api/types";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import { AuditLogStreamHeader } from "../components/AuditLogStreamHeader";
|
||||
import { AzureProviderAuditLogStreamForm } from "./AzureProviderAuditLogStreamForm";
|
||||
import { CriblProviderAuditLogStreamForm } from "./CriblProviderAuditLogStreamForm";
|
||||
import { CustomProviderAuditLogStreamForm } from "./CustomProviderAuditLogStreamForm";
|
||||
import { DatadogProviderAuditLogStreamForm } from "./DatadogProviderAuditLogStreamForm";
|
||||
import { SplunkProviderAuditLogStreamForm } from "./SplunkProviderAuditLogStreamForm";
|
||||
@@ -44,6 +46,10 @@ const CreateForm = ({ provider, onComplete }: CreateFormProps) => {
|
||||
};
|
||||
|
||||
switch (provider) {
|
||||
case LogProvider.Azure:
|
||||
return <AzureProviderAuditLogStreamForm onSubmit={onSubmit} />;
|
||||
case LogProvider.Cribl:
|
||||
return <CriblProviderAuditLogStreamForm onSubmit={onSubmit} />;
|
||||
case LogProvider.Custom:
|
||||
return <CustomProviderAuditLogStreamForm onSubmit={onSubmit} />;
|
||||
case LogProvider.Datadog:
|
||||
@@ -83,6 +89,14 @@ const UpdateForm = ({ auditLogStream, onComplete }: UpdateFormProps) => {
|
||||
};
|
||||
|
||||
switch (auditLogStream.provider) {
|
||||
case LogProvider.Azure:
|
||||
return (
|
||||
<AzureProviderAuditLogStreamForm onSubmit={onSubmit} auditLogStream={auditLogStream} />
|
||||
);
|
||||
case LogProvider.Cribl:
|
||||
return (
|
||||
<CriblProviderAuditLogStreamForm onSubmit={onSubmit} auditLogStream={auditLogStream} />
|
||||
);
|
||||
case LogProvider.Custom:
|
||||
return (
|
||||
<CustomProviderAuditLogStreamForm onSubmit={onSubmit} auditLogStream={auditLogStream} />
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, SecretInput } from "@app/components/v2";
|
||||
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
|
||||
import { TAzureProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/azure-provider";
|
||||
|
||||
type Props = {
|
||||
auditLogStream?: TAzureProviderLogStream;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.literal(LogProvider.Azure),
|
||||
credentials: z.object({
|
||||
tenantId: z.string().trim().uuid(),
|
||||
clientId: z.string().trim().uuid(),
|
||||
clientSecret: z.string().trim().length(40),
|
||||
dceUrl: z.string().trim().url().min(1).max(255),
|
||||
dcrId: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^dcr-[0-9a-f]{32}$/, "DCR ID must be in dcr-*** format"),
|
||||
cltName: z.string().trim().min(1).max(255)
|
||||
})
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const AzureProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(auditLogStream);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: auditLogStream ?? {
|
||||
provider: LogProvider.Azure
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="credentials.tenantId"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.clientId"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Client ID"
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.clientSecret"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Client Secret"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.dceUrl"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Data Collection Endpoint URL"
|
||||
>
|
||||
<Input {...field} placeholder="https://example.eastus-1.ingest.monitor.azure.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.dcrId"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Data Collection Rule Immutable ID"
|
||||
>
|
||||
<Input {...field} placeholder="dcr-00000000000000000000000000000000" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.cltName"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Custom Log Table Name"
|
||||
>
|
||||
<Input {...field} placeholder="InfisicalLogs" />
|
||||
</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" : "Create Log Stream"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, SecretInput } from "@app/components/v2";
|
||||
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
|
||||
import { TCriblProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/cribl-provider";
|
||||
|
||||
type Props = {
|
||||
auditLogStream?: TCriblProviderLogStream;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.literal(LogProvider.Cribl),
|
||||
credentials: z.object({
|
||||
url: z.string().url().trim().min(1).max(255),
|
||||
token: z.string().trim().min(21).max(255)
|
||||
})
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const CriblProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(auditLogStream);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: auditLogStream ?? {
|
||||
provider: LogProvider.Cribl
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="credentials.url"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Cribl Stream URL"
|
||||
tooltipText={
|
||||
<>
|
||||
To derive your Stream URL: Obtain your Cribl hostname (e.g. cribl.example.com),
|
||||
Infisical HTTP data source port (e.g. 20000), and HTTP event API path (e.g.
|
||||
/infisical).
|
||||
<br />
|
||||
<br />
|
||||
If your Infisical Data Source has TLS enabled, then use the https protocol.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="http://default.main.example.cribl.cloud:20000/infisical/_bulk"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.token"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Cribl Stream 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" : "Create Log Stream"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+53
-17
@@ -24,11 +24,17 @@ enum EnforceAuthType {
|
||||
export const OrgGeneralAuthSection = ({
|
||||
isSamlConfigured,
|
||||
isOidcConfigured,
|
||||
isGoogleConfigured
|
||||
isGoogleConfigured,
|
||||
isSamlActive,
|
||||
isOidcActive,
|
||||
isLdapActive
|
||||
}: {
|
||||
isSamlConfigured: boolean;
|
||||
isOidcConfigured: boolean;
|
||||
isGoogleConfigured: boolean;
|
||||
isSamlActive: boolean;
|
||||
isOidcActive: boolean;
|
||||
isLdapActive: boolean;
|
||||
}) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
@@ -126,6 +132,15 @@ export const OrgGeneralAuthSection = ({
|
||||
}
|
||||
};
|
||||
|
||||
const isGoogleOAuthEnforced = currentOrg.googleSsoAuthEnforced;
|
||||
|
||||
const getActiveSsoLabel = () => {
|
||||
if (isSamlActive) return "SAML";
|
||||
if (isOidcActive) return "OIDC";
|
||||
if (isLdapActive) return "LDAP";
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div>
|
||||
@@ -135,7 +150,7 @@ export const OrgGeneralAuthSection = ({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 py-4">
|
||||
<div className={twMerge("mt-4", !isSamlConfigured && "hidden")}>
|
||||
<div className={twMerge("mt-4", (!isSamlConfigured || isGoogleOAuthEnforced) && "hidden")}>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
|
||||
@@ -160,7 +175,7 @@ export const OrgGeneralAuthSection = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={twMerge("mt-4", !isOidcConfigured && "hidden")}>
|
||||
<div className={twMerge("mt-4", (!isOidcConfigured || isGoogleOAuthEnforced) && "hidden")}>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enforce OIDC SSO</span>
|
||||
@@ -188,26 +203,47 @@ export const OrgGeneralAuthSection = ({
|
||||
<div className={twMerge("mt-2", !isGoogleConfigured && "hidden")}>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enforce Google SSO</span>
|
||||
<span className="text-md text-mineshaft-100">Enforce Google OAuth</span>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Sso}
|
||||
tooltipProps={{
|
||||
className: "max-w-sm",
|
||||
side: "left"
|
||||
}}
|
||||
allowedLabel={
|
||||
isOidcActive || isSamlActive || isLdapActive
|
||||
? `You cannot enforce Google OAuth while ${getActiveSsoLabel()} SSO is enabled. Disable ${getActiveSsoLabel()} SSO to enforce Google OAuth.`
|
||||
: undefined
|
||||
}
|
||||
renderTooltip={isOidcActive || isSamlActive || isLdapActive}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enforce-google-sso"
|
||||
onCheckedChange={(value) =>
|
||||
handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE)
|
||||
}
|
||||
isChecked={currentOrg?.googleSsoAuthEnforced ?? false}
|
||||
isDisabled={!isAllowed || currentOrg?.authEnforced}
|
||||
/>
|
||||
<div>
|
||||
<Switch
|
||||
id="enforce-google-sso"
|
||||
onCheckedChange={(value) =>
|
||||
handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE)
|
||||
}
|
||||
isChecked={currentOrg?.googleSsoAuthEnforced ?? false}
|
||||
isDisabled={
|
||||
!isAllowed ||
|
||||
currentOrg?.authEnforced ||
|
||||
isOidcActive ||
|
||||
isSamlActive ||
|
||||
isLdapActive
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Enforce users to authenticate via Google OAuth SSO to access this organization.
|
||||
Enforce users to authenticate via Google OAuth to access this organization.
|
||||
<br />
|
||||
When this is enabled your organization members will only be able to login with Google
|
||||
SSO (not Google SAML).
|
||||
OAuth (not Google SAML).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,8 +303,8 @@ export const OrgGeneralAuthSection = ({
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
<span>
|
||||
Allow organization admins to bypass SAML enforcement when SSO is unavailable,
|
||||
misconfigured, or inaccessible.
|
||||
Allow organization admins to bypass SSO login enforcement when your SSO provider is
|
||||
unavailable, misconfigured, or inaccessible.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+26
-9
@@ -94,6 +94,8 @@ export const OrgLDAPSection = (): JSX.Element => {
|
||||
handlePopUpOpen("ldapGroupMap");
|
||||
};
|
||||
|
||||
const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced;
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="py-4">
|
||||
@@ -116,16 +118,31 @@ export const OrgLDAPSection = (): JSX.Element => {
|
||||
<div className="pt-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">Enable LDAP</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Ldap}
|
||||
tooltipProps={{
|
||||
className: "max-w-sm",
|
||||
side: "left"
|
||||
}}
|
||||
allowedLabel={
|
||||
isGoogleOAuthEnabled
|
||||
? "You cannot enable LDAP SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable LDAP SSO."
|
||||
: undefined
|
||||
}
|
||||
renderTooltip={isGoogleOAuthEnabled}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleLDAPToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Enable
|
||||
</Switch>
|
||||
<div>
|
||||
<Switch
|
||||
id="enable-ldap-sso"
|
||||
onCheckedChange={(value) => handleLDAPToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
isDisabled={!isAllowed || isGoogleOAuthEnabled}
|
||||
>
|
||||
Enable
|
||||
</Switch>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
|
||||
+24
-7
@@ -83,6 +83,8 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced;
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
@@ -106,14 +108,29 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">Enable OIDC</h2>
|
||||
{!isPending && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Sso}
|
||||
tooltipProps={{
|
||||
className: "max-w-sm",
|
||||
side: "left"
|
||||
}}
|
||||
allowedLabel={
|
||||
isGoogleOAuthEnabled
|
||||
? "You cannot enable OIDC SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable OIDC SSO."
|
||||
: undefined
|
||||
}
|
||||
renderTooltip={isGoogleOAuthEnabled}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enable-oidc-sso"
|
||||
onCheckedChange={(value) => handleOIDCToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
<div>
|
||||
<Switch
|
||||
id="enable-oidc-sso"
|
||||
onCheckedChange={(value) => handleOIDCToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
isDisabled={!isAllowed || isGoogleOAuthEnabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
|
||||
+24
-7
@@ -78,6 +78,8 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
@@ -99,14 +101,29 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
<div className="mb-2 flex items-center justify-between pt-4">
|
||||
<h2 className="text-md text-mineshaft-100">Enable SAML</h2>
|
||||
{!isPending && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Sso}
|
||||
tooltipProps={{
|
||||
className: "max-w-sm",
|
||||
side: "left"
|
||||
}}
|
||||
allowedLabel={
|
||||
isGoogleOAuthEnabled
|
||||
? "You cannot enable SAML SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable SAML SSO."
|
||||
: undefined
|
||||
}
|
||||
renderTooltip={isGoogleOAuthEnabled}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
<div>
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
isDisabled={!isAllowed || isGoogleOAuthEnabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
|
||||
@@ -184,6 +184,9 @@ export const OrgSsoTab = withPermission(
|
||||
isSamlConfigured={isSamlConfigured}
|
||||
isOidcConfigured={isOidcConfigured}
|
||||
isGoogleConfigured={isGoogleConfigured}
|
||||
isSamlActive={Boolean(samlConfig?.isActive)}
|
||||
isOidcActive={Boolean(oidcConfig?.isActive)}
|
||||
isLdapActive={Boolean(ldapConfig?.isActive)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+25
-1
@@ -19,6 +19,7 @@ import {
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
@@ -66,6 +67,7 @@ const formSchema = z.object({
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
sslEnabled: z.boolean().optional(),
|
||||
ca: z.string().optional(),
|
||||
gatewayId: z.string().optional()
|
||||
}),
|
||||
@@ -200,6 +202,7 @@ export const SqlDatabaseInputForm = ({
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
const selectedClient = watch("provider.client");
|
||||
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
@@ -458,13 +461,34 @@ export const SqlDatabaseInputForm = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{selectedClient === SqlProviders.MsSQL && (
|
||||
<div className="mb-2 mt-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.sslEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="sql-ds-ssl-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
>
|
||||
Encrypt Connection (SSL)
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA(SSL)"
|
||||
label="CA (SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
|
||||
+25
-1
@@ -18,6 +18,7 @@ import {
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
@@ -63,6 +64,7 @@ const formSchema = z.object({
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
sslEnabled: z.boolean().optional(),
|
||||
ca: z.string().optional(),
|
||||
gatewayId: z.string().optional().nullable()
|
||||
})
|
||||
@@ -151,6 +153,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
});
|
||||
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
const selectedClient = watch("inputs.client");
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
const selectedGatewayId = watch("inputs.gatewayId");
|
||||
@@ -407,13 +410,34 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{selectedClient === SqlProviders.MsSQL && (
|
||||
<div className="mb-2 mt-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.sslEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="sql-ds-ssl-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={Boolean(value)}
|
||||
onCheckedChange={onChange}
|
||||
>
|
||||
Encrypt Connection (SSL)
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA(SSL)"
|
||||
label="CA (SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import { useState } from "react";
|
||||
import ReactCodeInput from "react-code-input";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { useRequestEmailChangeOTP, useUpdateUserEmail } from "@app/hooks/api/users";
|
||||
import { clearSession } from "@app/hooks/api/users/queries";
|
||||
|
||||
const emailSchema = z
|
||||
.object({
|
||||
newEmail: z.string().email("Please enter a valid email")
|
||||
})
|
||||
.required();
|
||||
|
||||
export type EmailFormData = z.infer<typeof emailSchema>;
|
||||
|
||||
const otpInputProps = {
|
||||
inputStyle: {
|
||||
fontFamily: "monospace",
|
||||
margin: "4px",
|
||||
MozAppearance: "textfield" as const,
|
||||
width: "45px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "18px",
|
||||
height: "45px",
|
||||
padding: "0",
|
||||
paddingLeft: "0",
|
||||
paddingRight: "0",
|
||||
backgroundColor: "#262626",
|
||||
color: "white",
|
||||
border: "1px solid #404040",
|
||||
textAlign: "center" as const,
|
||||
outlineColor: "#8ca542",
|
||||
borderColor: "#404040"
|
||||
}
|
||||
};
|
||||
|
||||
export const ChangeEmailSection = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUser();
|
||||
const [isOTPModalOpen, setIsOTPModalOpen] = useState(false);
|
||||
const [pendingEmail, setPendingEmail] = useState("");
|
||||
|
||||
const emailForm = useForm<EmailFormData>({
|
||||
defaultValues: { newEmail: "" },
|
||||
resolver: zodResolver(emailSchema)
|
||||
});
|
||||
|
||||
const { mutateAsync: requestEmailChangeOTP, isPending: isRequestingOTP } =
|
||||
useRequestEmailChangeOTP();
|
||||
const { mutateAsync: updateUserEmail, isPending: isUpdatingEmail } = useUpdateUserEmail();
|
||||
|
||||
// Watch the email field to enable/disable the button
|
||||
const watchedEmail = useWatch({
|
||||
control: emailForm.control,
|
||||
name: "newEmail",
|
||||
defaultValue: ""
|
||||
});
|
||||
|
||||
// Helper function to check if email is valid
|
||||
const isEmailValid = (email: string): boolean => {
|
||||
try {
|
||||
emailSchema.parse({ newEmail: email });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailSubmit = async ({ newEmail }: EmailFormData) => {
|
||||
if (newEmail.toLowerCase() === user?.email?.toLowerCase()) {
|
||||
createNotification({
|
||||
text: "New email must be different from current email",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await requestEmailChangeOTP({ newEmail });
|
||||
setPendingEmail(newEmail);
|
||||
setIsOTPModalOpen(true);
|
||||
|
||||
createNotification({
|
||||
text: "Verification code sent to your new email address. Check your inbox!",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
const errorMessage = err?.response?.data?.message || "Failed to send verification code";
|
||||
createNotification({
|
||||
text: errorMessage,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [typedOTP, setTypedOTP] = useState("");
|
||||
|
||||
const handleOTPSubmit = async () => {
|
||||
if (typedOTP.length !== 6) {
|
||||
createNotification({
|
||||
text: "Please enter the complete 6-digit verification code",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateUserEmail({ newEmail: pendingEmail, otpCode: typedOTP });
|
||||
|
||||
createNotification({
|
||||
text: "Email updated successfully. You will be redirected to login.",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
// Reset forms and close modal
|
||||
emailForm.reset();
|
||||
setIsOTPModalOpen(false);
|
||||
setPendingEmail("");
|
||||
setTypedOTP("");
|
||||
|
||||
// Clear frontend session/token to ensure proper logout
|
||||
clearSession(true);
|
||||
|
||||
// Redirect to login after a short delay
|
||||
setTimeout(() => {
|
||||
navigate({ to: "/login" });
|
||||
}, 2000);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
|
||||
const errorMessage = err?.response?.data?.message || "Invalid verification code";
|
||||
if (errorMessage.includes("Invalid verification code")) {
|
||||
// Reset to email step so user must request new OTP
|
||||
setIsOTPModalOpen(false);
|
||||
setPendingEmail("");
|
||||
setTypedOTP("");
|
||||
emailForm.reset();
|
||||
|
||||
createNotification({
|
||||
text: "Invalid verification code. Please request a new one.",
|
||||
type: "error"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
text: errorMessage,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOTPModalClose = () => {
|
||||
setIsOTPModalOpen(false);
|
||||
setPendingEmail("");
|
||||
setTypedOTP("");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Change email</h2>
|
||||
|
||||
<form onSubmit={emailForm.handleSubmit(handleEmailSubmit)}>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
control={emailForm.control}
|
||||
name="newEmail"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="New email address"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter new email address"
|
||||
type="email"
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isRequestingOTP}
|
||||
isDisabled={isRequestingOTP || !isEmailValid(watchedEmail)}
|
||||
>
|
||||
Send Verification Code
|
||||
</Button>
|
||||
<p className="mt-2 font-inter text-sm text-mineshaft-400">
|
||||
We'll send an 6-digit verification code to your new email address.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={isOTPModalOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) handleOTPModalClose();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Email Verification"
|
||||
subTitle={`Enter the 6-digit verification code sent to: ${pendingEmail}`}
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<ReactCodeInput
|
||||
name="otp-input"
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setTypedOTP}
|
||||
value={typedOTP}
|
||||
{...otpInputProps}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button colorSchema="secondary" variant="outline" onClick={handleOTPModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleOTPSubmit}
|
||||
isLoading={isUpdatingEmail}
|
||||
isDisabled={typedOTP.length !== 6}
|
||||
>
|
||||
Confirm Email Change
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ChangeEmailSection } from "./ChangeEmailSection";
|
||||
+2
@@ -2,6 +2,7 @@ import { useGetUser } from "@app/hooks/api";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
|
||||
import { AuthMethodSection } from "../AuthMethodSection";
|
||||
import { ChangeEmailSection } from "../ChangeEmailSection";
|
||||
import { ChangePasswordSection } from "../ChangePasswordSection";
|
||||
import { MFASection } from "../SecuritySection";
|
||||
|
||||
@@ -16,6 +17,7 @@ export const PersonalAuthTab = () => {
|
||||
</>
|
||||
)}
|
||||
<ChangePasswordSection />
|
||||
{user && !user.authMethods.includes(AuthMethod.LDAP) && <ChangeEmailSection />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user