mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 23:16:55 +00:00
Merge remote-tracking branch 'origin/main' into feat/gateway-v2
This commit is contained in:
+5
-1
@@ -148,7 +148,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
accessTokenTTL: 2592000,
|
||||
accessTokenMaxTTL: 2592000,
|
||||
accessTokenNumUsesLimit: 0,
|
||||
accessTokenPeriod: 0
|
||||
accessTokenPeriod: 0,
|
||||
lockoutEnabled: true,
|
||||
lockoutThreshold: 3,
|
||||
lockoutDurationSeconds: 300,
|
||||
lockoutCounterResetSeconds: 30
|
||||
});
|
||||
|
||||
handlePopUpToggle("identity", false);
|
||||
|
||||
+330
-13
@@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -11,12 +12,16 @@ import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { getObjectFromSeconds } from "@app/helpers/datetime";
|
||||
import {
|
||||
useAddIdentityUniversalAuth,
|
||||
useGetIdentityUniversalAuth,
|
||||
@@ -60,9 +65,79 @@ const schema = z
|
||||
ipAddress: z.string().max(50)
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.min(1),
|
||||
lockoutEnabled: z.boolean().default(true),
|
||||
lockoutThreshold: z
|
||||
.string()
|
||||
.refine(
|
||||
(value) => Number(value) <= 30 && Number(value) >= 1,
|
||||
"Lockout threshold must be between 1 and 30"
|
||||
),
|
||||
lockoutDurationValue: z.string(),
|
||||
lockoutDurationUnit: z.enum(["s", "m", "h", "d"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
}),
|
||||
lockoutCounterResetValue: z.string(),
|
||||
lockoutCounterResetUnit: z.enum(["s", "m", "h"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
})
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.superRefine((data, ctx) => {
|
||||
const {
|
||||
lockoutDurationValue,
|
||||
lockoutCounterResetValue,
|
||||
lockoutDurationUnit,
|
||||
lockoutCounterResetUnit,
|
||||
lockoutEnabled
|
||||
} = data;
|
||||
|
||||
if (!lockoutEnabled) return;
|
||||
|
||||
let isAnyParseError = false;
|
||||
|
||||
const parsedLockoutDuration = parseInt(lockoutDurationValue, 10);
|
||||
if (Number.isNaN(parsedLockoutDuration)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout duration must be a number",
|
||||
path: ["lockoutDurationValue"]
|
||||
});
|
||||
isAnyParseError = true;
|
||||
}
|
||||
|
||||
const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10);
|
||||
if (Number.isNaN(parsedLockoutCounterReset)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout counter reset must be a number",
|
||||
path: ["lockoutCounterResetValue"]
|
||||
});
|
||||
isAnyParseError = true;
|
||||
}
|
||||
|
||||
if (isAnyParseError) return;
|
||||
|
||||
const lockoutDurationInSeconds = ms(`${parsedLockoutDuration}${lockoutDurationUnit}`) / 1000;
|
||||
const lockoutCounterResetInSeconds =
|
||||
ms(`${parsedLockoutCounterReset}${lockoutCounterResetUnit}`) / 1000;
|
||||
|
||||
if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout duration must be between 30 seconds and 1 day",
|
||||
path: ["lockoutDurationValue"]
|
||||
});
|
||||
}
|
||||
|
||||
if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Lockout counter reset must be between 5 seconds and 1 hour",
|
||||
path: ["lockoutCounterResetValue"]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
@@ -107,12 +182,25 @@ export const IdentityUniversalAuthForm = ({
|
||||
accessTokenNumUsesLimit: "0",
|
||||
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
accessTokenPeriod: "0"
|
||||
accessTokenPeriod: "0",
|
||||
lockoutEnabled: true,
|
||||
lockoutThreshold: "3",
|
||||
lockoutDurationValue: "5",
|
||||
lockoutDurationUnit: "m",
|
||||
lockoutCounterResetValue: "30",
|
||||
lockoutCounterResetUnit: "s"
|
||||
}
|
||||
});
|
||||
|
||||
const accessTokenPeriodValue = Number(watch("accessTokenPeriod"));
|
||||
|
||||
const lockoutEnabledWatch = watch("lockoutEnabled");
|
||||
const lockoutThresholdWatch = watch("lockoutThreshold");
|
||||
const lockoutDurationValueWatch = watch("lockoutDurationValue");
|
||||
const lockoutDurationUnitWatch = watch("lockoutDurationUnit");
|
||||
const lockoutCounterResetValueWatch = watch("lockoutCounterResetValue");
|
||||
const lockoutCounterResetUnitWatch = watch("lockoutCounterResetUnit");
|
||||
|
||||
const {
|
||||
fields: clientSecretTrustedIpsFields,
|
||||
append: appendClientSecretTrustedIp,
|
||||
@@ -126,6 +214,9 @@ export const IdentityUniversalAuthForm = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const lockoutDurationObj = getObjectFromSeconds(data.lockoutDurationSeconds);
|
||||
const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterResetSeconds);
|
||||
|
||||
reset({
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
@@ -144,7 +235,13 @@ export const IdentityUniversalAuthForm = ({
|
||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
};
|
||||
}
|
||||
)
|
||||
),
|
||||
lockoutEnabled: data.lockoutEnabled,
|
||||
lockoutThreshold: String(data.lockoutThreshold),
|
||||
lockoutDurationValue: String(lockoutDurationObj.value),
|
||||
lockoutDurationUnit: lockoutDurationObj.unit as "s" | "m" | "h" | "d",
|
||||
lockoutCounterResetValue: String(lockoutCounterResetObj.value),
|
||||
lockoutCounterResetUnit: lockoutCounterResetObj.unit as "s" | "m" | "h"
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
@@ -153,7 +250,13 @@ export const IdentityUniversalAuthForm = ({
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenPeriod: "0",
|
||||
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
lockoutEnabled: true,
|
||||
lockoutThreshold: "3",
|
||||
lockoutDurationValue: "5",
|
||||
lockoutDurationUnit: "m",
|
||||
lockoutCounterResetValue: "30",
|
||||
lockoutCounterResetUnit: "s"
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
@@ -164,11 +267,21 @@ export const IdentityUniversalAuthForm = ({
|
||||
accessTokenNumUsesLimit,
|
||||
clientSecretTrustedIps,
|
||||
accessTokenTrustedIps,
|
||||
accessTokenPeriod
|
||||
accessTokenPeriod,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDurationValue,
|
||||
lockoutDurationUnit,
|
||||
lockoutCounterResetValue,
|
||||
lockoutCounterResetUnit
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!identityId) return;
|
||||
|
||||
const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000;
|
||||
const lockoutCounterResetSeconds =
|
||||
ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000;
|
||||
|
||||
if (data) {
|
||||
// update universal auth configuration
|
||||
await updateMutateAsync({
|
||||
@@ -179,7 +292,11 @@ export const IdentityUniversalAuthForm = ({
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps,
|
||||
accessTokenPeriod: Number(accessTokenPeriod)
|
||||
accessTokenPeriod: Number(accessTokenPeriod),
|
||||
lockoutEnabled,
|
||||
lockoutThreshold: Number(lockoutThreshold),
|
||||
lockoutDurationSeconds,
|
||||
lockoutCounterResetSeconds
|
||||
});
|
||||
} else {
|
||||
// create new universal auth configuration
|
||||
@@ -192,7 +309,11 @@ export const IdentityUniversalAuthForm = ({
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps,
|
||||
accessTokenPeriod: Number(accessTokenPeriod)
|
||||
accessTokenPeriod: Number(accessTokenPeriod),
|
||||
lockoutEnabled,
|
||||
lockoutThreshold: Number(lockoutThreshold),
|
||||
lockoutDurationSeconds: Number(lockoutDurationSeconds),
|
||||
lockoutCounterResetSeconds: Number(lockoutCounterResetSeconds)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,16 +338,31 @@ export const IdentityUniversalAuthForm = ({
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
["accessTokenTrustedIps", "clientSecretTrustedIps"].includes(Object.keys(fields)[0])
|
||||
? IdentityFormTab.Advanced
|
||||
: IdentityFormTab.Configuration
|
||||
);
|
||||
const firstErrorField = Object.keys(fields)[0];
|
||||
let tab = IdentityFormTab.Configuration;
|
||||
|
||||
if (["accessTokenTrustedIps", "clientSecretTrustedIps"].includes(firstErrorField)) {
|
||||
tab = IdentityFormTab.Advanced;
|
||||
} else if (
|
||||
[
|
||||
"lockoutEnabled",
|
||||
"lockoutThreshold",
|
||||
"lockoutDurationValue",
|
||||
"lockoutDurationUnit",
|
||||
"lockoutCounterResetValue",
|
||||
"lockoutCounterResetUnit"
|
||||
].includes(firstErrorField)
|
||||
) {
|
||||
tab = IdentityFormTab.Lockout;
|
||||
}
|
||||
|
||||
setTabValue(tab);
|
||||
})}
|
||||
>
|
||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||
<TabList>
|
||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||
<Tab value={IdentityFormTab.Lockout}>Lockout</Tab>
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
@@ -296,6 +432,187 @@ export const IdentityUniversalAuthForm = ({
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Lockout}>
|
||||
<div className="mb-3 flex flex-col">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutEnabled"
|
||||
defaultValue
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={`The lockout feature will prevent login attempts for ${lockoutDurationValueWatch}${lockoutDurationUnitWatch} after ${lockoutThresholdWatch} consecutive login failures. If ${lockoutCounterResetValueWatch}${lockoutCounterResetUnitWatch} pass after the most recent failure, the lockout counter resets.`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="ml-0 mr-3 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
containerClassName="flex-row-reverse w-fit"
|
||||
id="lockout-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
Lockout
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutThreshold"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
label="Lockout Threshold"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The amount of times login must fail before locking the identity auth method"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout threshold..."
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-end gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutDurationValue"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
label="Lockout Duration"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="How long an identity auth method lockout lasts"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout duration..."
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutDurationUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={`mb-0 ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
value={field.value}
|
||||
className="min-w-32 pr-2"
|
||||
onValueChange={field.onChange}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value="s"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Seconds</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="m"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Minutes</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="h"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Hours</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="d"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Days</div>
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutCounterResetValue"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
label="Lockout Counter Reset"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="How long to wait from the most recent failed login until resetting the lockout counter"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter lockout counter reset..."
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lockoutCounterResetUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={`mb-0 ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
isDisabled={!lockoutEnabledWatch}
|
||||
value={field.value}
|
||||
className="min-w-32 pr-2"
|
||||
onValueChange={field.onChange}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value="s"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Seconds</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="m"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Minutes</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="h"
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">Hours</div>
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
export enum IdentityFormTab {
|
||||
Advanced = "advanced",
|
||||
Lockout = "lockout",
|
||||
Configuration = "configuration"
|
||||
}
|
||||
|
||||
+5
@@ -12,6 +12,7 @@ import { AppConnectionHeader } from "../AppConnectionHeader";
|
||||
import { OnePassConnectionForm } from "./1PasswordConnectionForm";
|
||||
import { Auth0ConnectionForm } from "./Auth0ConnectionForm";
|
||||
import { AwsConnectionForm } from "./AwsConnectionForm";
|
||||
import { AzureADCSConnectionForm } from "./AzureADCSConnectionForm";
|
||||
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
|
||||
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
|
||||
import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
|
||||
@@ -96,6 +97,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureAppConfiguration:
|
||||
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureADCS:
|
||||
return <AzureADCSConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Databricks:
|
||||
return <DatabricksConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Humanitec:
|
||||
@@ -208,6 +211,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return (
|
||||
<AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} />
|
||||
);
|
||||
case AppConnection.AzureADCS:
|
||||
return <AzureADCSConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.Databricks:
|
||||
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Humanitec:
|
||||
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { AzureADCSConnectionMethod, TAzureADCSConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureADCSConnection;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.AzureADCS)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
|
||||
credentials: z.object({
|
||||
adcsUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "ADCS URL required")
|
||||
.refine((value) => value.startsWith("https://"), "ADCS URL must use HTTPS"),
|
||||
username: z.string().trim().min(1, "Username required"),
|
||||
password: z.string().trim().min(1, "Password required"),
|
||||
sslRejectUnauthorized: z.boolean().optional(),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const AzureADCSConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.AzureADCS,
|
||||
method: AzureADCSConnectionMethod.UsernamePassword,
|
||||
name: "",
|
||||
description: "",
|
||||
credentials: {
|
||||
adcsUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
watch
|
||||
} = form;
|
||||
|
||||
const sslEnabled = watch("credentials.adcsUrl")?.startsWith("https://") ?? false;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
setSelectedTabIndex(0);
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
{!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.AzureADCS].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(AzureADCSConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List
|
||||
className={`-pb-1 ${selectedTabIndex === 1 ? "mb-3" : "mb-6"} w-full border-b-2 border-mineshaft-600`}
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Configuration
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
SSL ({sslEnabled ? "Enabled" : "Disabled"})
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
{selectedTabIndex === 1 && (
|
||||
<div className="mb-2 text-xs text-mineshaft-300">
|
||||
SSL configuration for HTTPS connections
|
||||
</div>
|
||||
)}
|
||||
<Tab.Panels className="mb-4 rounded border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
|
||||
<Tab.Panel>
|
||||
<Controller
|
||||
name="credentials.adcsUrl"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="ADCS URL"
|
||||
>
|
||||
<Input {...field} placeholder="https://your-adcs-server.com/certsrv" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Controller
|
||||
name="credentials.username"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Username"
|
||||
>
|
||||
<Input {...field} placeholder="DOMAIN\\username or [email protected]" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.password"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Password"
|
||||
>
|
||||
<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>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<Controller
|
||||
name="credentials.sslCertificate"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
label="SSL Certificate"
|
||||
isOptional
|
||||
>
|
||||
<TextArea
|
||||
className="h-[3.6rem] !resize-none"
|
||||
{...field}
|
||||
isDisabled={!sslEnabled}
|
||||
placeholder="-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
-----END CERTIFICATE-----"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.sslRejectUnauthorized"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="ssl-reject-unauthorized"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={sslEnabled ? value : false}
|
||||
onCheckedChange={onChange}
|
||||
isDisabled={!sslEnabled}
|
||||
>
|
||||
<p className="w-[9.5rem]">
|
||||
Reject Unauthorized
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<p>
|
||||
If enabled, Infisical will only connect to the ADCS server if it has a
|
||||
valid, trusted SSL certificate. Disable only in test environments with
|
||||
self-signed certificates.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
<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 Azure ADCS"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+63
-2
@@ -1,7 +1,9 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
@@ -9,9 +11,16 @@ import {
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||
import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
@@ -66,7 +75,8 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.HCVault,
|
||||
method: HCVaultConnectionMethod.AppRole
|
||||
method: HCVaultConnectionMethod.AppRole,
|
||||
gatewayId: null
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,6 +89,9 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
@@ -115,6 +128,54 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{subscription.gateway && (
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.AttachGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="gatewayId"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
label="Gateway"
|
||||
>
|
||||
<Tooltip
|
||||
isDisabled={isAllowed}
|
||||
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||
>
|
||||
<Select
|
||||
isDisabled={!isAllowed}
|
||||
value={value as string}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isLoading={isGatewaysLoading}
|
||||
placeholder="Default: Internet Gateway"
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value={null as unknown as string}
|
||||
onClick={() => onChange(undefined)}
|
||||
>
|
||||
Internet Gateway
|
||||
</SelectItem>
|
||||
{gateways?.map((el) => (
|
||||
<SelectItem value={el.id} key={el.id}>
|
||||
{el.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<Controller
|
||||
name="credentials.instanceUrl"
|
||||
control={control}
|
||||
|
||||
@@ -4,9 +4,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import {
|
||||
OrgPermissionAuditLogsActions,
|
||||
OrgPermissionSubjects,
|
||||
ProjectPermissionAuditLogsActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { Timezone } from "@app/helpers/datetime";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { withPermission, withProjectPermission } from "@app/hoc";
|
||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -28,153 +34,173 @@ type Props = {
|
||||
project?: Workspace;
|
||||
};
|
||||
|
||||
export const LogsSection = withPermission(
|
||||
({ presets, refetchInterval, showFilters = true, pageView = false, project }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const LogsSectionComponent = ({
|
||||
presets,
|
||||
refetchInterval,
|
||||
showFilters = true,
|
||||
pageView = false,
|
||||
project
|
||||
}: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
const [logFilter, setLogFilter] = useState<TAuditLogFilterFormData>({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
eventMetadata: presets?.eventMetadata
|
||||
});
|
||||
const [timezone, setTimezone] = useState<Timezone>(Timezone.Local);
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
const [logFilter, setLogFilter] = useState<TAuditLogFilterFormData>({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
eventMetadata: presets?.eventMetadata
|
||||
});
|
||||
const [timezone, setTimezone] = useState<Timezone>(Timezone.Local);
|
||||
const [dateFilter, setDateFilter] = useState<TAuditLogDateFilterFormData>(
|
||||
presets?.endDate || presets?.startDate
|
||||
? {
|
||||
type: AuditLogDateFilterType.Absolute,
|
||||
startDate: presets?.startDate || new Date(Number(new Date()) - ms("1h")),
|
||||
endDate: presets?.endDate || new Date()
|
||||
}
|
||||
: {
|
||||
startDate: new Date(Number(new Date()) - ms("1h")),
|
||||
endDate: new Date(),
|
||||
type: AuditLogDateFilterType.Relative,
|
||||
relativeModeValue: "1h"
|
||||
}
|
||||
);
|
||||
|
||||
const [dateFilter, setDateFilter] = useState<TAuditLogDateFilterFormData>(
|
||||
presets?.endDate || presets?.startDate
|
||||
? {
|
||||
type: AuditLogDateFilterType.Absolute,
|
||||
startDate: presets?.startDate || new Date(Number(new Date()) - ms("1h")),
|
||||
endDate: presets?.endDate || new Date()
|
||||
}
|
||||
: {
|
||||
startDate: new Date(Number(new Date()) - ms("1h")),
|
||||
endDate: new Date(),
|
||||
type: AuditLogDateFilterType.Relative,
|
||||
relativeModeValue: "1h"
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.auditLogs) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
if (pageView)
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-y-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Audit History</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/audit-logs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.1rem] 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.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
{showFilters && (
|
||||
<LogsDateFilter
|
||||
filter={dateFilter}
|
||||
setFilter={setDateFilter}
|
||||
timezone={timezone}
|
||||
setTimezone={setTimezone}
|
||||
/>
|
||||
)}
|
||||
{showFilters && (
|
||||
<LogsFilter
|
||||
project={project}
|
||||
presets={presets}
|
||||
setFilter={setLogFilter}
|
||||
filter={logFilter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
filter={{
|
||||
secretPath: logFilter.secretPath || undefined,
|
||||
secretKey: logFilter.secretKey || undefined,
|
||||
eventMetadata: logFilter?.eventMetadata,
|
||||
projectId: project?.id || logFilter?.project?.id,
|
||||
actorType: presets?.actorType,
|
||||
limit: 15,
|
||||
eventType: logFilter?.eventType,
|
||||
userAgentType: logFilter?.userAgentType,
|
||||
startDate: dateFilter?.startDate,
|
||||
endDate: dateFilter?.endDate,
|
||||
environment: logFilter?.environment?.slug,
|
||||
actor: logFilter?.actor
|
||||
}}
|
||||
timezone={timezone}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.auditLogs) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
if (pageView)
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
{showFilters && (
|
||||
<LogsDateFilter
|
||||
filter={dateFilter}
|
||||
setFilter={setDateFilter}
|
||||
timezone={timezone}
|
||||
setTimezone={setTimezone}
|
||||
/>
|
||||
)}
|
||||
{showFilters && (
|
||||
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
|
||||
)}
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-y-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Audit History</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/audit-logs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.1rem] 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.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
{showFilters && (
|
||||
<LogsDateFilter
|
||||
filter={dateFilter}
|
||||
setFilter={setDateFilter}
|
||||
timezone={timezone}
|
||||
setTimezone={setTimezone}
|
||||
/>
|
||||
)}
|
||||
{showFilters && (
|
||||
<LogsFilter
|
||||
project={project}
|
||||
presets={presets}
|
||||
setFilter={setLogFilter}
|
||||
filter={logFilter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
filter={{
|
||||
secretPath: logFilter.secretPath || undefined,
|
||||
secretKey: logFilter.secretKey || undefined,
|
||||
eventMetadata: logFilter?.eventMetadata,
|
||||
projectId: project?.id || logFilter?.project?.id,
|
||||
actorType: presets?.actorType,
|
||||
limit: 15,
|
||||
eventType: logFilter?.eventType,
|
||||
userAgentType: logFilter?.userAgentType,
|
||||
startDate: dateFilter?.startDate,
|
||||
endDate: dateFilter?.endDate,
|
||||
environment: logFilter?.environment?.slug,
|
||||
actor: logFilter?.actor
|
||||
}}
|
||||
timezone={timezone}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
/>
|
||||
</div>
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
filter={{
|
||||
secretPath: logFilter.secretPath || undefined,
|
||||
secretKey: logFilter.secretKey || undefined,
|
||||
eventMetadata: logFilter?.eventMetadata,
|
||||
projectId: logFilter?.project?.id,
|
||||
actorType: presets?.actorType,
|
||||
limit: 15,
|
||||
eventType: logFilter?.eventType,
|
||||
userAgentType: logFilter?.userAgentType,
|
||||
startDate: dateFilter?.startDate,
|
||||
endDate: dateFilter?.endDate,
|
||||
environment: logFilter?.environment?.slug,
|
||||
actor: logFilter?.actor
|
||||
}}
|
||||
timezone={timezone}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.AuditLogs }
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
{showFilters && (
|
||||
<LogsDateFilter
|
||||
filter={dateFilter}
|
||||
setFilter={setDateFilter}
|
||||
timezone={timezone}
|
||||
setTimezone={setTimezone}
|
||||
/>
|
||||
)}
|
||||
{showFilters && (
|
||||
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
|
||||
)}
|
||||
</div>
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
filter={{
|
||||
secretPath: logFilter.secretPath || undefined,
|
||||
secretKey: logFilter.secretKey || undefined,
|
||||
eventMetadata: logFilter?.eventMetadata,
|
||||
projectId: project?.id || logFilter?.project?.id,
|
||||
actorType: presets?.actorType,
|
||||
limit: 15,
|
||||
eventType: logFilter?.eventType,
|
||||
userAgentType: logFilter?.userAgentType,
|
||||
startDate: dateFilter?.startDate,
|
||||
endDate: dateFilter?.endDate,
|
||||
environment: logFilter?.environment?.slug,
|
||||
actor: logFilter?.actor
|
||||
}}
|
||||
timezone={timezone}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogsSection = (props: Props) => {
|
||||
const { project } = props;
|
||||
|
||||
if (project) {
|
||||
const ProjectLogsSectionWithPermission = withProjectPermission(LogsSectionComponent, {
|
||||
action: ProjectPermissionAuditLogsActions.Read,
|
||||
subject: ProjectPermissionSub.AuditLogs
|
||||
});
|
||||
return <ProjectLogsSectionWithPermission {...props} />;
|
||||
}
|
||||
|
||||
const OrgLogsSectionWithPermission = withPermission(LogsSectionComponent, {
|
||||
action: OrgPermissionAuditLogsActions.Read,
|
||||
subject: OrgPermissionSubjects.AuditLogs
|
||||
});
|
||||
return <OrgLogsSectionWithPermission {...props} />;
|
||||
};
|
||||
|
||||
@@ -115,8 +115,10 @@ const Page = () => {
|
||||
<ViewIdentityAuthModal
|
||||
isOpen={popUp.viewAuthMethod.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
|
||||
authMethod={popUp.viewAuthMethod.data}
|
||||
authMethod={popUp.viewAuthMethod.data?.authMethod}
|
||||
lockedOut={popUp.viewAuthMethod.data?.lockedOut || false}
|
||||
identityId={identityId}
|
||||
onResetAllLockouts={popUp.viewAuthMethod.data?.refetchIdentity}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+18
-5
@@ -1,8 +1,8 @@
|
||||
import { faCog, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCog, faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { Button, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -16,7 +16,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
|
||||
const { data } = useGetIdentityById(identityId);
|
||||
const { data, refetch } = useGetIdentityById(identityId);
|
||||
|
||||
return data ? (
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
@@ -28,12 +28,25 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
|
||||
{data.identity.authMethods.map((authMethod) => (
|
||||
<button
|
||||
key={authMethod}
|
||||
onClick={() => handlePopUpOpen("viewAuthMethod", authMethod)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("viewAuthMethod", {
|
||||
authMethod,
|
||||
lockedOut: data.identity.activeLockoutAuthMethods.includes(authMethod),
|
||||
refetchIdentity: refetch
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between bg-mineshaft-900 px-4 py-2 text-sm hover:bg-mineshaft-700 data-[state=open]:bg-mineshaft-600"
|
||||
>
|
||||
<span>{identityAuthToNameMap[authMethod]}</span>
|
||||
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
|
||||
<div className="flex gap-2">
|
||||
{data.identity.activeLockoutAuthMethods.includes(authMethod) && (
|
||||
<Tooltip content="Auth method has active lockouts">
|
||||
<FontAwesomeIcon icon={faLock} size="xs" className="text-red-400/50" />
|
||||
</Tooltip>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+16
-3
@@ -37,9 +37,11 @@ import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthCon
|
||||
type Props = {
|
||||
identityId: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
lockedOut: boolean;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
onDeleteAuthMethod: () => void;
|
||||
onResetAllLockouts: () => void;
|
||||
};
|
||||
|
||||
type TRevokeOptions = {
|
||||
@@ -50,8 +52,13 @@ type TRevokeOptions = {
|
||||
export const Content = ({
|
||||
identityId,
|
||||
authMethod,
|
||||
onDeleteAuthMethod
|
||||
}: Pick<Props, "authMethod" | "identityId" | "onDeleteAuthMethod">) => {
|
||||
lockedOut,
|
||||
onDeleteAuthMethod,
|
||||
onResetAllLockouts
|
||||
}: Pick<
|
||||
Props,
|
||||
"authMethod" | "lockedOut" | "identityId" | "onDeleteAuthMethod" | "onResetAllLockouts"
|
||||
>) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
@@ -159,9 +166,11 @@ export const Content = ({
|
||||
<Component
|
||||
identityId={identityId}
|
||||
onDelete={handleDelete}
|
||||
onResetAllLockouts={onResetAllLockouts}
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
lockedOut={lockedOut}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
||||
@@ -184,7 +193,9 @@ export const ViewIdentityAuthModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
authMethod,
|
||||
identityId
|
||||
identityId,
|
||||
lockedOut,
|
||||
onResetAllLockouts
|
||||
}: Omit<Props, "onDeleteAuthMethod">) => {
|
||||
if (!identityId || !authMethod) return null;
|
||||
|
||||
@@ -194,7 +205,9 @@ export const ViewIdentityAuthModal = ({
|
||||
<Content
|
||||
identityId={identityId}
|
||||
authMethod={authMethod}
|
||||
lockedOut={lockedOut}
|
||||
onDeleteAuthMethod={() => onOpenChange(false)}
|
||||
onResetAllLockouts={() => onResetAllLockouts()}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
+67
-2
@@ -1,9 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
|
||||
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import {
|
||||
useClearIdentityUniversalAuthLockouts,
|
||||
useGetIdentityUniversalAuth,
|
||||
useGetIdentityUniversalAuthClientSecrets
|
||||
} from "@app/hooks/api";
|
||||
@@ -19,16 +25,40 @@ export const ViewIdentityUniversalAuthContent = ({
|
||||
handlePopUpToggle,
|
||||
handlePopUpOpen,
|
||||
onDelete,
|
||||
popUp
|
||||
popUp,
|
||||
lockedOut,
|
||||
onResetAllLockouts
|
||||
}: ViewAuthMethodProps) => {
|
||||
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
|
||||
const { data: clientSecrets = [], isPending: clientSecretsPending } =
|
||||
useGetIdentityUniversalAuthClientSecrets(identityId);
|
||||
const { mutateAsync: clearLockoutsFn, isPending: isClearLockoutsPending } =
|
||||
useClearIdentityUniversalAuthLockouts();
|
||||
|
||||
const [lockedOutState, setLockedOutState] = useState(lockedOut);
|
||||
|
||||
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
|
||||
initialState: "Copy Client ID to clipboard"
|
||||
});
|
||||
|
||||
async function clearLockouts() {
|
||||
try {
|
||||
const deleted = await clearLockoutsFn({ identityId });
|
||||
createNotification({
|
||||
text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`,
|
||||
type: "success"
|
||||
});
|
||||
setLockedOutState(false);
|
||||
onResetAllLockouts();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to clear lockouts. Please try again.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isPending || clientSecretsPending) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
@@ -85,6 +115,41 @@ export const ViewIdentityUniversalAuthContent = ({
|
||||
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
|
||||
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout">
|
||||
{data.lockoutEnabled ? "Enabled" : "Disabled"}
|
||||
</IdentityAuthFieldDisplay>
|
||||
{data.lockoutEnabled && (
|
||||
<>
|
||||
<div className="col-span-2 mt-3 flex justify-between border-b border-mineshaft-500 pb-2">
|
||||
<span className="text-bunker-300">Lockout Options</span>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed || !lockedOutState || isClearLockoutsPending}
|
||||
size="xs"
|
||||
onClick={() => clearLockouts()}
|
||||
isLoading={isClearLockoutsPending}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Reset All Lockouts
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<IdentityAuthFieldDisplay label="Lockout Threshold">
|
||||
{data.lockoutThreshold}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout Duration">
|
||||
{ms(data.lockoutDurationSeconds * 1000, { long: true })}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Lockout Counter Reset">
|
||||
{ms(data.lockoutCounterResetSeconds * 1000, { long: true })}
|
||||
</IdentityAuthFieldDisplay>
|
||||
</>
|
||||
)}
|
||||
<div className="col-span-2 my-3">
|
||||
<div className="mb-3 border-b border-mineshaft-500 pb-2">
|
||||
<span className="text-bunker-300">Client ID</span>
|
||||
|
||||
+2
@@ -9,4 +9,6 @@ export type ViewAuthMethodProps = {
|
||||
state?: boolean
|
||||
) => void;
|
||||
popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>;
|
||||
lockedOut: boolean;
|
||||
onResetAllLockouts: () => void;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AllProjectView } from "./components/AllProjectView";
|
||||
import { MyProjectView } from "./components/MyProjectView";
|
||||
import { ProjectListToggle, ProjectListView } from "./components/ProjectListToggle";
|
||||
import { ProjectListView } from "./components/ProjectListToggle";
|
||||
|
||||
// const formatDescription = (type: ProjectType) => {
|
||||
// if (type === ProjectType.SecretManager)
|
||||
@@ -28,7 +28,23 @@ import { ProjectListToggle, ProjectListView } from "./components/ProjectListTogg
|
||||
export const ProjectsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [projectListView, setProjectListView] = useState(ProjectListView.MyProjects);
|
||||
const [projectListView, setProjectListView] = useState<ProjectListView>(() => {
|
||||
const storedView = localStorage.getItem("projectListView");
|
||||
|
||||
if (
|
||||
storedView &&
|
||||
(storedView === ProjectListView.AllProjects || storedView === ProjectListView.MyProjects)
|
||||
) {
|
||||
return storedView;
|
||||
}
|
||||
|
||||
return ProjectListView.MyProjects;
|
||||
});
|
||||
|
||||
const handleSetProjectListView = (value: ProjectListView) => {
|
||||
localStorage.setItem("projectListView", value);
|
||||
setProjectListView(value);
|
||||
};
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
@@ -49,11 +65,7 @@ export const ProjectsPage = () => {
|
||||
</Helmet>
|
||||
<div className="mb-4 flex flex-col items-start justify-start">
|
||||
<PageHeader
|
||||
title={
|
||||
<div className="flex items-center gap-4">
|
||||
<ProjectListToggle value={projectListView} onChange={setProjectListView} />
|
||||
</div>
|
||||
}
|
||||
title="Projects"
|
||||
description="Your team's complete security toolkit - organized and ready when you need them."
|
||||
/>
|
||||
</div>
|
||||
@@ -62,12 +74,16 @@ export const ProjectsPage = () => {
|
||||
onAddNewProject={() => handlePopUpOpen("addNewWs")}
|
||||
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
|
||||
isAddingProjectsAllowed={isAddingProjectsAllowed}
|
||||
projectListView={projectListView}
|
||||
onProjectListViewChange={handleSetProjectListView}
|
||||
/>
|
||||
) : (
|
||||
<AllProjectView
|
||||
onAddNewProject={() => handlePopUpOpen("addNewWs")}
|
||||
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
|
||||
isAddingProjectsAllowed={isAddingProjectsAllowed}
|
||||
projectListView={projectListView}
|
||||
onProjectListViewChange={handleSetProjectListView}
|
||||
/>
|
||||
)}
|
||||
<NewProjectModal
|
||||
|
||||
@@ -48,12 +48,18 @@ import {
|
||||
useRequestProjectAccess,
|
||||
useSearchProjects
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||
import { ProjectType, Workspace, WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||
import {
|
||||
ProjectListToggle,
|
||||
ProjectListView
|
||||
} from "@app/pages/organization/ProjectsPage/components/ProjectListToggle";
|
||||
|
||||
type Props = {
|
||||
onAddNewProject: () => void;
|
||||
onUpgradePlan: () => void;
|
||||
isAddingProjectsAllowed: boolean;
|
||||
projectListView: ProjectListView;
|
||||
onProjectListViewChange: (value: ProjectListView) => void;
|
||||
};
|
||||
|
||||
type RequestAccessModalProps = {
|
||||
@@ -106,7 +112,9 @@ const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProp
|
||||
export const AllProjectView = ({
|
||||
onAddNewProject,
|
||||
onUpgradePlan,
|
||||
isAddingProjectsAllowed
|
||||
isAddingProjectsAllowed,
|
||||
projectListView,
|
||||
onProjectListViewChange
|
||||
}: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
@@ -144,13 +152,17 @@ export const AllProjectView = ({
|
||||
type: projectTypeFilter
|
||||
});
|
||||
|
||||
const handleAccessProject = async (type: ProjectType, projectId: string) => {
|
||||
const handleAccessProject = async (
|
||||
type: ProjectType,
|
||||
projectId: string,
|
||||
environments: WorkspaceEnv[]
|
||||
) => {
|
||||
try {
|
||||
await orgAdminAccessProject.mutateAsync({
|
||||
projectId
|
||||
});
|
||||
await navigate({
|
||||
to: getProjectHomePage(type),
|
||||
to: getProjectHomePage(type, environments),
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -176,10 +188,10 @@ export const AllProjectView = ({
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex-grow" />
|
||||
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
containerClassName="w-full"
|
||||
containerClassName="w-full ml-2"
|
||||
placeholder="Search by project name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
@@ -242,7 +254,7 @@ export const AllProjectView = ({
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<div className="ml-2 flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<Tooltip content="Disabled across All Project view.">
|
||||
<div className="flex cursor-not-allowed items-center justify-center">
|
||||
<IconButton
|
||||
@@ -307,7 +319,7 @@ export const AllProjectView = ({
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter" && workspace.isMember) {
|
||||
navigate({
|
||||
to: getProjectHomePage(workspace.type),
|
||||
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||
params: {
|
||||
projectId: workspace.id
|
||||
}
|
||||
@@ -317,7 +329,7 @@ export const AllProjectView = ({
|
||||
onClick={() => {
|
||||
if (workspace.isMember) {
|
||||
navigate({
|
||||
to: getProjectHomePage(workspace.type),
|
||||
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||
params: {
|
||||
projectId: workspace.id
|
||||
}
|
||||
@@ -363,7 +375,7 @@ export const AllProjectView = ({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleAccessProject(workspace.type, workspace.id);
|
||||
handleAccessProject(workspace.type, workspace.id, workspace.environments);
|
||||
}}
|
||||
disabled={
|
||||
orgAdminAccessProject.variables?.projectId === workspace.id &&
|
||||
|
||||
@@ -44,11 +44,17 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||
import {
|
||||
ProjectListToggle,
|
||||
ProjectListView
|
||||
} from "@app/pages/organization/ProjectsPage/components/ProjectListToggle";
|
||||
|
||||
type Props = {
|
||||
onAddNewProject: () => void;
|
||||
onUpgradePlan: () => void;
|
||||
isAddingProjectsAllowed: boolean;
|
||||
projectListView: ProjectListView;
|
||||
onProjectListViewChange: (value: ProjectListView) => void;
|
||||
};
|
||||
|
||||
enum ProjectOrderBy {
|
||||
@@ -63,7 +69,9 @@ enum ProjectsViewMode {
|
||||
export const MyProjectView = ({
|
||||
onAddNewProject,
|
||||
onUpgradePlan,
|
||||
isAddingProjectsAllowed
|
||||
isAddingProjectsAllowed,
|
||||
projectListView,
|
||||
onProjectListViewChange
|
||||
}: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
@@ -185,7 +193,7 @@ export const MyProjectView = ({
|
||||
<div
|
||||
onClick={() => {
|
||||
navigate({
|
||||
to: getProjectHomePage(workspace.type),
|
||||
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||
params: {
|
||||
projectId: workspace.id
|
||||
}
|
||||
@@ -239,7 +247,7 @@ export const MyProjectView = ({
|
||||
<div
|
||||
onClick={() => {
|
||||
navigate({
|
||||
to: getProjectHomePage(workspace.type),
|
||||
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||
params: {
|
||||
projectId: workspace.id
|
||||
}
|
||||
@@ -371,10 +379,10 @@ export const MyProjectView = ({
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex-grow" />
|
||||
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
containerClassName="w-full"
|
||||
containerClassName="w-full ml-2"
|
||||
placeholder="Search by project name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
@@ -441,7 +449,7 @@ export const MyProjectView = ({
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<div className="ml-2 flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Select, SelectItem } from "@app/components/v2";
|
||||
import { Button } from "@app/components/v2";
|
||||
|
||||
export enum ProjectListView {
|
||||
MyProjects = "my-projects",
|
||||
@@ -14,28 +11,32 @@ type Props = {
|
||||
};
|
||||
|
||||
export const ProjectListToggle = ({ value, onChange }: Props) => {
|
||||
const getDisplayText = (listView: ProjectListView) => {
|
||||
return listView === ProjectListView.MyProjects ? "My Projects" : "All Projects";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group relative flex cursor-pointer items-center gap-2">
|
||||
<h1 className="text-3xl font-semibold transition-colors group-hover:text-gray-500">
|
||||
{getDisplayText(value)}
|
||||
</h1>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0"
|
||||
position="popper"
|
||||
<div className="flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(ProjectListView.MyProjects);
|
||||
}}
|
||||
size="xs"
|
||||
className={`${
|
||||
value === ProjectListView.MyProjects ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
<SelectItem value={ProjectListView.MyProjects}>My Projects</SelectItem>
|
||||
<SelectItem value={ProjectListView.AllProjects}>All Projects</SelectItem>
|
||||
</Select>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronDown}
|
||||
className="text-lg transition-colors group-hover:text-gray-500"
|
||||
/>
|
||||
My Projects
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(ProjectListView.AllProjects);
|
||||
}}
|
||||
size="xs"
|
||||
className={`${
|
||||
value === ProjectListView.AllProjects ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
All Projects
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-1
@@ -5,6 +5,7 @@ import { OrgPermissionSubjects } from "@app/context";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionAuditLogsActions,
|
||||
OrgPermissionBillingActions,
|
||||
OrgPermissionGroupActions,
|
||||
OrgPermissionIdentityActions,
|
||||
@@ -23,6 +24,12 @@ const generalPermissionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const auditLogsPermissionSchema = z
|
||||
.object({
|
||||
[OrgPermissionAuditLogsActions.Read]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
const billingPermissionSchema = z
|
||||
.object({
|
||||
[OrgPermissionBillingActions.Read]: z.boolean().optional(),
|
||||
@@ -121,7 +128,7 @@ export const formSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
|
||||
"audit-logs": generalPermissionSchema,
|
||||
"audit-logs": auditLogsPermissionSchema,
|
||||
member: generalPermissionSchema,
|
||||
groups: groupPermissionSchema,
|
||||
role: generalPermissionSchema,
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { OrgPermissionAuditLogsActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSION_ACTIONS = [
|
||||
{
|
||||
action: OrgPermissionAuditLogsActions.Read,
|
||||
label: "Read"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const OrgPermissionAuditLogsRow = ({ isEditable, control, setValue }: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.audit-logs"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||
if (isRowCustom) {
|
||||
setIsRowExpanded.on();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (!val) return;
|
||||
if (val === Permission.Custom) {
|
||||
setIsRowExpanded.on();
|
||||
setIsCustom.on();
|
||||
return;
|
||||
}
|
||||
setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
default:
|
||||
setValue(
|
||||
"permissions.audit-logs",
|
||||
{
|
||||
[OrgPermissionAuditLogsActions.Read]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td className="w-4">
|
||||
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||
</Td>
|
||||
<Td className="w-full select-none">Audit Logs</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="h-8 w-40 bg-mineshaft-700"
|
||||
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isRowExpanded && (
|
||||
<Tr>
|
||||
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
|
||||
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`permissions.audit-logs.${action}`}
|
||||
key={`permissions.audit-logs.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={(e) => {
|
||||
if (!isEditable) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update default role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
id={`permissions.audit-logs.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -71,6 +71,7 @@ type Props = {
|
||||
| "gateway"
|
||||
| "secret-share"
|
||||
| "billing"
|
||||
| "audit-logs"
|
||||
| "machine-identity-auth-template"
|
||||
>;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
|
||||
+6
-4
@@ -16,6 +16,7 @@ import {
|
||||
TFormSchema
|
||||
} from "../OrgRoleModifySection.utils";
|
||||
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||
import { OrgPermissionAuditLogsRow } from "./OrgPermissionAuditLogsRow";
|
||||
import { OrgPermissionBillingRow } from "./OrgPermissionBillingRow";
|
||||
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
|
||||
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
|
||||
@@ -39,10 +40,6 @@ const SIMPLE_PERMISSION_OPTIONS = [
|
||||
title: "Incident Contacts",
|
||||
formName: "incident-contact"
|
||||
},
|
||||
{
|
||||
title: "Audit Logs",
|
||||
formName: "audit-logs"
|
||||
},
|
||||
{
|
||||
title: "Organization Profile",
|
||||
formName: "settings"
|
||||
@@ -166,6 +163,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<OrgPermissionAuditLogsRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgPermissionIdentityRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
|
||||
+14
-77
@@ -1,70 +1,19 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { getObjectFromSeconds } from "@app/helpers/datetime";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const MAX_SHARED_SECRET_LIFETIME_SECONDS = 30 * 24 * 60 * 60; // 30 days in seconds
|
||||
const MIN_SHARED_SECRET_LIFETIME_SECONDS = 5 * 60; // 5 minutes in seconds
|
||||
|
||||
// Helper function to convert duration to seconds
|
||||
const durationToSeconds = (value: number, unit: "m" | "h" | "d"): number => {
|
||||
switch (unit) {
|
||||
case "m":
|
||||
return value * 60;
|
||||
case "h":
|
||||
return value * 60 * 60;
|
||||
case "d":
|
||||
return value * 60 * 60 * 24;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to convert seconds to form lifetime value and unit
|
||||
const getFormLifetimeFromSeconds = (
|
||||
totalSeconds: number | null | undefined
|
||||
): { maxLifetimeValue: number; maxLifetimeUnit: "m" | "h" | "d" } => {
|
||||
const DEFAULT_LIFETIME_VALUE = 30;
|
||||
const DEFAULT_LIFETIME_UNIT = "d" as "m" | "h" | "d";
|
||||
|
||||
if (totalSeconds == null || totalSeconds <= 0) {
|
||||
return {
|
||||
maxLifetimeValue: DEFAULT_LIFETIME_VALUE,
|
||||
maxLifetimeUnit: DEFAULT_LIFETIME_UNIT
|
||||
};
|
||||
}
|
||||
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
const secondsInHour = 60 * 60;
|
||||
const secondsInMinute = 60;
|
||||
|
||||
if (totalSeconds % secondsInDay === 0) {
|
||||
const value = totalSeconds / secondsInDay;
|
||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "d" };
|
||||
}
|
||||
|
||||
if (totalSeconds % secondsInHour === 0) {
|
||||
const value = totalSeconds / secondsInHour;
|
||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "h" };
|
||||
}
|
||||
|
||||
if (totalSeconds % secondsInMinute === 0) {
|
||||
const value = totalSeconds / secondsInMinute;
|
||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "m" };
|
||||
}
|
||||
|
||||
return {
|
||||
maxLifetimeValue: DEFAULT_LIFETIME_VALUE,
|
||||
maxLifetimeUnit: DEFAULT_LIFETIME_UNIT
|
||||
};
|
||||
};
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
maxLifetimeValue: z.number().min(1, "Value must be at least 1"),
|
||||
@@ -77,34 +26,20 @@ const formSchema = z
|
||||
.superRefine((data, ctx) => {
|
||||
const { maxLifetimeValue, maxLifetimeUnit } = data;
|
||||
|
||||
const durationInSeconds = durationToSeconds(maxLifetimeValue, maxLifetimeUnit);
|
||||
const durationInSeconds = ms(`${maxLifetimeValue}${maxLifetimeUnit}`) / 1000;
|
||||
|
||||
// Check max limit
|
||||
if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
|
||||
let message = "Duration exceeds maximum allowed limit";
|
||||
|
||||
if (maxLifetimeUnit === "m") {
|
||||
message = `Maximum allowed minutes is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / 60} (30 days)`;
|
||||
} else if (maxLifetimeUnit === "h") {
|
||||
message = `Maximum allowed hours is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / (60 * 60)} (30 days)`;
|
||||
} else if (maxLifetimeUnit === "d") {
|
||||
message = `Maximum allowed days is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / (24 * 60 * 60)}`;
|
||||
}
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message,
|
||||
message: "Duration exceeds a maximum of 30 days",
|
||||
path: ["maxLifetimeValue"]
|
||||
});
|
||||
}
|
||||
|
||||
// Check min limit
|
||||
if (durationInSeconds < MIN_SHARED_SECRET_LIFETIME_SECONDS) {
|
||||
const message = `Duration must be at least ${MIN_SHARED_SECRET_LIFETIME_SECONDS / 60} minutes`; // 5 minutes
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message,
|
||||
message: "Duration must be at least 5 minutes",
|
||||
path: ["maxLifetimeValue"]
|
||||
});
|
||||
}
|
||||
@@ -122,10 +57,14 @@ export const OrgSecretShareLimitSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const getDefaultFormValues = () => {
|
||||
const initialLifetime = getFormLifetimeFromSeconds(currentOrg?.maxSharedSecretLifetime);
|
||||
const initialLifetime = getObjectFromSeconds(currentOrg?.maxSharedSecretLifetime, [
|
||||
"m",
|
||||
"h",
|
||||
"d"
|
||||
]);
|
||||
return {
|
||||
maxLifetimeValue: initialLifetime.maxLifetimeValue,
|
||||
maxLifetimeUnit: initialLifetime.maxLifetimeUnit,
|
||||
maxLifetimeValue: initialLifetime.value,
|
||||
maxLifetimeUnit: initialLifetime.unit as "m" | "h" | "d",
|
||||
maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "1",
|
||||
shouldLimitView: Boolean(currentOrg?.maxSharedSecretViewLimit)
|
||||
};
|
||||
@@ -152,10 +91,8 @@ export const OrgSecretShareLimitSection = () => {
|
||||
|
||||
const handleFormSubmit = async (formData: TForm) => {
|
||||
try {
|
||||
const maxSharedSecretLifetimeSeconds = durationToSeconds(
|
||||
formData.maxLifetimeValue,
|
||||
formData.maxLifetimeUnit
|
||||
);
|
||||
const maxSharedSecretLifetimeSeconds =
|
||||
ms(`${formData.maxLifetimeValue}${formData.maxLifetimeUnit}`) / 1000;
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
|
||||
+12
-8
@@ -1,6 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { faKey, faVault } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
@@ -20,14 +18,16 @@ enum WizardSteps {
|
||||
|
||||
const PLATFORM_LIST = [
|
||||
{
|
||||
icon: faKey,
|
||||
image: "/images/integrations/EnvKey.png",
|
||||
platform: "env-key",
|
||||
title: "Env Key"
|
||||
title: "EnvKey",
|
||||
size: 34
|
||||
},
|
||||
{
|
||||
icon: faVault,
|
||||
image: "/images/integrations/Vault.png",
|
||||
platform: "vault",
|
||||
title: "Vault"
|
||||
title: "HCP Vault",
|
||||
size: 40
|
||||
}
|
||||
] as const;
|
||||
|
||||
@@ -67,7 +67,7 @@ export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => {
|
||||
{PLATFORM_LIST.map((platform, idx) => (
|
||||
<div
|
||||
key={`platform-${idx + 1}`}
|
||||
className="flex h-28 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||
className="flex h-28 w-32 cursor-pointer flex-col items-center justify-between rounded border border-mineshaft-500 bg-mineshaft-700 p-6 py-5 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
@@ -81,7 +81,11 @@ export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={platform.icon} size="lg" />
|
||||
<img
|
||||
src={platform.image}
|
||||
alt={`${platform.title} logo`}
|
||||
style={{ width: platform.size }}
|
||||
/>
|
||||
<div className="whitespace-pre-wrap text-center text-sm">{platform.title}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
+73
-11
@@ -2,12 +2,19 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||
import { useImportVault } from "@app/hooks/api/migration/mutations";
|
||||
|
||||
type Props = {
|
||||
@@ -62,36 +69,32 @@ const MAPPING_TYPE_MENU_ITEMS = [
|
||||
export const VaultPlatformModal = ({ onClose }: Props) => {
|
||||
const formSchema = z.object({
|
||||
vaultUrl: z.string().min(1),
|
||||
gatewayId: z.string().optional(),
|
||||
vaultNamespace: z.string().trim().optional(),
|
||||
vaultAccessToken: z.string().min(1),
|
||||
mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault)
|
||||
});
|
||||
type TFormData = z.infer<typeof formSchema>;
|
||||
|
||||
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
const { mutateAsync: importVault } = useImportVault();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isLoading, isDirty, isSubmitting, isValid, errors }
|
||||
formState: { isLoading, isDirty, isSubmitting, isValid }
|
||||
} = useForm<TFormData>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
console.log({
|
||||
isSubmitting,
|
||||
isLoading,
|
||||
isValid,
|
||||
errors
|
||||
});
|
||||
|
||||
const onSubmit = async (data: TFormData) => {
|
||||
await importVault({
|
||||
vaultAccessToken: data.vaultAccessToken,
|
||||
vaultNamespace: data.vaultNamespace,
|
||||
vaultUrl: data.vaultUrl,
|
||||
mappingType: data.mappingType
|
||||
mappingType: data.mappingType,
|
||||
...(data.gatewayId && { gatewayId: data.gatewayId })
|
||||
});
|
||||
createNotification({
|
||||
title: "Import started",
|
||||
@@ -110,11 +113,70 @@ export const VaultPlatformModal = ({ onClose }: Props) => {
|
||||
The Vault migration currently supports importing static secrets from Vault
|
||||
Dedicated/Self-Hosted.
|
||||
<div className="mt-2 text-xs opacity-80">
|
||||
Currently only KV Secret Engine V2 is supported for Vault migrations.
|
||||
Currently only KV Secret Engine is supported for Vault migrations.
|
||||
</div>
|
||||
</p>
|
||||
</NoticeBannerV2>
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<div className="w-full flex-1">
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.AttachGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="gatewayId"
|
||||
defaultValue=""
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
label="Gateway"
|
||||
isOptional
|
||||
>
|
||||
<Tooltip
|
||||
isDisabled={isAllowed}
|
||||
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||
>
|
||||
<div>
|
||||
<Select
|
||||
isDisabled={!isAllowed}
|
||||
value={value as string}
|
||||
onValueChange={(v) => {
|
||||
if (v !== "") {
|
||||
onChange(v);
|
||||
}
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isLoading={isGatewayLoading}
|
||||
placeholder="Default: Internet Gateway"
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value={undefined as unknown as string}
|
||||
onClick={() => {
|
||||
onChange(undefined);
|
||||
}}
|
||||
>
|
||||
Internet Gateway
|
||||
</SelectItem>
|
||||
{gateways?.map((el) => (
|
||||
<SelectItem value={el.id} key={el.id}>
|
||||
{el.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="vaultUrl"
|
||||
|
||||
+6
-6
@@ -51,7 +51,7 @@ export const GithubOrgSyncConfigModal = ({
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
values: data ? { githubOrgName: data.githubOrgName } : undefined
|
||||
values: data ? { githubOrgName: data.githubOrgName, githubOrgAccessToken: "" } : undefined
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ githubOrgName, githubOrgAccessToken }: FormData) => {
|
||||
@@ -123,21 +123,21 @@ export const GithubOrgSyncConfigModal = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{/* <Controller
|
||||
<Controller
|
||||
control={control}
|
||||
name="githubOrgAccessToken"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="GitHub Org Scoped Access Token"
|
||||
label="GitHub Access Token"
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
helperText="A GitHub access token is required only for private organizations. It will not be visible after saving."
|
||||
helperText="Required for manual sync operations. The token must have 'read:org' permissions."
|
||||
>
|
||||
<Input {...field} placeholder="example" />
|
||||
<Input {...field} type="password" placeholder="ghp_xxxxxxxxxxxx" />
|
||||
</FormControl>
|
||||
)}
|
||||
/> */}
|
||||
/>
|
||||
<div className="flex gap-8 pt-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||
{isUpdate ? "Update" : "Configure"}
|
||||
|
||||
+96
-1
@@ -1,10 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, Modal, ModalContent, Skeleton, Spinner, Switch } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { githubOrgSyncConfigQueryKeys, useUpdateGithubSyncOrgConfig } from "@app/hooks/api";
|
||||
import {
|
||||
githubOrgSyncConfigQueryKeys,
|
||||
useSyncAllGithubTeams,
|
||||
useUpdateGithubSyncOrgConfig
|
||||
} from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { GithubOrgSyncConfigModal } from "./GithubOrgSyncConfigModal";
|
||||
@@ -24,10 +29,73 @@ export const OrgGithubSyncSection = () => {
|
||||
});
|
||||
|
||||
const updateGithubSyncOrgConfig = useUpdateGithubSyncOrgConfig();
|
||||
const syncAllTeamsMutation = useSyncAllGithubTeams();
|
||||
|
||||
const isPending = subscription.githubOrgSync && githubOrgSyncConfig.isPending;
|
||||
const data = !isPending && !githubOrgSyncConfig?.isError ? githubOrgSyncConfig?.data : undefined;
|
||||
|
||||
const handleBulkSync = async () => {
|
||||
try {
|
||||
const result = await syncAllTeamsMutation.mutateAsync();
|
||||
let message = "Successfully synced teams";
|
||||
|
||||
const details = [];
|
||||
if (result.createdTeams.length > 0) {
|
||||
details.push(
|
||||
`${result.createdTeams.length} new team${result.createdTeams.length === 1 ? "" : "s"} created`
|
||||
);
|
||||
}
|
||||
if (result.updatedTeams.length > 0) {
|
||||
details.push(
|
||||
`${result.updatedTeams.length} team${result.updatedTeams.length === 1 ? "" : "s"} updated`
|
||||
);
|
||||
}
|
||||
if (result.removedMemberships > 0) {
|
||||
details.push(
|
||||
`${result.removedMemberships} membership${result.removedMemberships === 1 ? "" : "s"} removed`
|
||||
);
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
message += `. ${details.join(", ")}`;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: message,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
createNotification({
|
||||
text: `Sync completed with ${result.errors.length} warnings. Check the console for details.`,
|
||||
type: "warning"
|
||||
});
|
||||
console.warn("Sync errors:", result.errors);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
(error as any)?.response?.data?.message || (error as Error)?.message || "Unknown error";
|
||||
|
||||
if (
|
||||
errorMessage.includes("token") &&
|
||||
(errorMessage.includes("required") ||
|
||||
errorMessage.includes("invalid") ||
|
||||
errorMessage.includes("expired") ||
|
||||
errorMessage.includes("set a token first"))
|
||||
) {
|
||||
createNotification({
|
||||
text: "Please set a GitHub access token in the configuration modal to continue with the sync",
|
||||
type: "error"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
text: `Failed to sync GitHub teams: ${errorMessage}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<p className="text-xl font-semibold text-gray-200">
|
||||
@@ -86,6 +154,33 @@ export const OrgGithubSyncSection = () => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{data && data.isActive && (
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">Sync Now</h2>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.GithubOrgSyncManual}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handleBulkSync()}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isDisabled={!isAllowed || syncAllTeamsMutation.isPending}
|
||||
isLoading={syncAllTeamsMutation.isPending}
|
||||
>
|
||||
Sync Now
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Manually sync GitHub teams for all organization members. This will update team
|
||||
memberships for users who have previously logged in with GitHub.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
isOpen={popUp?.githubOrgSyncConfig?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
|
||||
+1
-1
@@ -1,5 +1,6 @@
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
} from "@app/context";
|
||||
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
enum EnforceAuthType {
|
||||
SAML = "saml",
|
||||
|
||||
Reference in New Issue
Block a user