mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 10:35:17 +00:00
Add merge conflicts
This commit is contained in:
@@ -30,11 +30,13 @@ export interface IsCliLoginSuccessful {
|
||||
const attemptLogin = async ({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
captchaToken?: string;
|
||||
}): Promise<IsCliLoginSuccessful> => {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -70,7 +72,8 @@ const attemptLogin = async ({
|
||||
} = await login2({
|
||||
email,
|
||||
clientProof,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
});
|
||||
if (mfaEnabled) {
|
||||
// case: MFA is enabled
|
||||
|
||||
@@ -22,11 +22,13 @@ interface IsLoginSuccessful {
|
||||
const attemptLogin = async ({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
captchaToken?: string;
|
||||
}): Promise<IsLoginSuccessful> => {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
// eslint-disable-next-line new-cap
|
||||
@@ -58,6 +60,7 @@ const attemptLogin = async ({
|
||||
iv,
|
||||
tag
|
||||
} = await login2({
|
||||
captchaToken,
|
||||
email,
|
||||
clientProof,
|
||||
providerAuthToken
|
||||
|
||||
@@ -2,5 +2,6 @@ const ENV = process.env.NEXT_PUBLIC_ENV! || "development"; // investigate
|
||||
const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
|
||||
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||
const INTERCOMid = process.env.NEXT_PUBLIC_INTERCOMid!;
|
||||
const CAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY!;
|
||||
|
||||
export { ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
|
||||
export { CAPTCHA_SITE_KEY, ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
|
||||
|
||||
@@ -78,7 +78,8 @@ export const SecretPathInput = ({
|
||||
const validPaths = inputValue.split("/");
|
||||
validPaths.pop();
|
||||
|
||||
const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}/`;
|
||||
// removed trailing slash
|
||||
const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`;
|
||||
onChange?.(newValue);
|
||||
setInputValue(newValue);
|
||||
setSecretPath(newValue);
|
||||
|
||||
@@ -93,27 +93,29 @@ const initProjectHelper = async ({ projectName }: { projectName: string }) => {
|
||||
});
|
||||
|
||||
try {
|
||||
secrets?.forEach((secret) => {
|
||||
createSecret({
|
||||
workspaceId: project.id,
|
||||
environment: secret.environment,
|
||||
type: secret.type,
|
||||
secretKey: secret.secretName,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
secretCommentCiphertext: secret.secretCommentCiphertext,
|
||||
secretCommentIV: secret.secretCommentIV,
|
||||
secretCommentTag: secret.secretCommentTag,
|
||||
secretPath: "/",
|
||||
metadata: {
|
||||
source: "signup"
|
||||
}
|
||||
});
|
||||
});
|
||||
await Promise.allSettled(
|
||||
(secrets || []).map((secret) =>
|
||||
createSecret({
|
||||
workspaceId: project.id,
|
||||
environment: secret.environment,
|
||||
type: secret.type,
|
||||
secretKey: secret.secretName,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
secretCommentCiphertext: secret.secretCommentCiphertext,
|
||||
secretCommentIV: secret.secretCommentIV,
|
||||
secretCommentTag: secret.secretCommentTag,
|
||||
secretPath: "/",
|
||||
metadata: {
|
||||
source: "signup"
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to upload secrets", err);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export type Login1DTO = {
|
||||
};
|
||||
|
||||
export type Login2DTO = {
|
||||
captchaToken?: string;
|
||||
email: string;
|
||||
clientProof: string;
|
||||
providerAuthToken?: string;
|
||||
|
||||
@@ -73,6 +73,9 @@ export const useCreateIntegration = () => {
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldMaskSecrets?: boolean;
|
||||
shouldProtectSecrets?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
|
||||
@@ -264,13 +264,12 @@ export const useGetImportedSecretsAllEnvs = ({
|
||||
});
|
||||
|
||||
const isImportedSecretPresentInEnv = useCallback(
|
||||
(secPath: string, envSlug: string, secretName: string) => {
|
||||
(envSlug: string, secretName: string) => {
|
||||
const selectedEnvIndex = environments.indexOf(envSlug);
|
||||
|
||||
if (selectedEnvIndex !== -1) {
|
||||
const isPresent = secretImports?.[selectedEnvIndex]?.data?.find(
|
||||
({ secretPath, secrets }) =>
|
||||
secretPath === secPath && secrets.some((s) => s.key === secretName)
|
||||
const isPresent = secretImports?.[selectedEnvIndex]?.data?.find(({ secrets }) =>
|
||||
secrets.some((s) => s.key === secretName)
|
||||
);
|
||||
|
||||
return Boolean(isPresent);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
TUpdateWorkspaceIdentityRoleDTO,
|
||||
TUpdateWorkspaceUserRoleDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
UpdatePitVersionLimitDTO,
|
||||
Workspace
|
||||
} from "./types";
|
||||
|
||||
@@ -258,6 +259,21 @@ export const useToggleAutoCapitalization = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateWorkspaceVersionLimit = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdatePitVersionLimitDTO>({
|
||||
mutationFn: ({ projectSlug, pitVersionLimit }) => {
|
||||
return apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, {
|
||||
pitVersionLimit
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export type Workspace = {
|
||||
upgradeStatus: string | null;
|
||||
autoCapitalization: boolean;
|
||||
environments: WorkspaceEnv[];
|
||||
pitVersionLimit: number;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
@@ -48,6 +49,7 @@ export type CreateWorkspaceDTO = {
|
||||
};
|
||||
|
||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||
|
||||
export type DeleteWorkspaceDTO = { workspaceID: string };
|
||||
@@ -128,4 +130,4 @@ export type TUpdateWorkspaceGroupRoleDTO = {
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
)[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
@@ -59,7 +60,7 @@ const schema = yup.object({
|
||||
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
|
||||
secretPath: yup.string().trim().required("Secrets Path is required"),
|
||||
secretSuffix: yup.string().trim().optional(),
|
||||
|
||||
shouldEnableDelete: yup.boolean().optional(),
|
||||
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
|
||||
|
||||
repoIds: yup.mixed().when("scope", {
|
||||
@@ -98,7 +99,6 @@ type FormData = yup.InferType<typeof schema>;
|
||||
export default function GitHubCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
|
||||
const integrationAuthId =
|
||||
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
|
||||
@@ -120,7 +120,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
defaultValues: {
|
||||
secretPath: "/",
|
||||
scope: "github-repo",
|
||||
repoIds: []
|
||||
repoIds: [],
|
||||
shouldEnableDelete: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,7 +178,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
app: targetApp.name, // repo name
|
||||
owner: targetApp.owner, // repo owner
|
||||
metadata: {
|
||||
secretSuffix: data.secretSuffix
|
||||
secretSuffix: data.secretSuffix,
|
||||
shouldEnableDelete: data.shouldEnableDelete
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -194,7 +196,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
scope: data.scope,
|
||||
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
|
||||
metadata: {
|
||||
secretSuffix: data.secretSuffix
|
||||
secretSuffix: data.secretSuffix,
|
||||
shouldEnableDelete: data.shouldEnableDelete
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -211,7 +214,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
owner: repoOwner,
|
||||
targetEnvironmentId: data.envId,
|
||||
metadata: {
|
||||
secretSuffix: data.secretSuffix
|
||||
secretSuffix: data.secretSuffix,
|
||||
shouldEnableDelete: data.shouldEnableDelete
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -546,6 +550,21 @@ export default function GitHubCreateIntegrationPage() {
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<div className="ml-1 mb-5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldEnableDelete"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="delete-github-option"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
Delete secrets in Github that are not in Infisical
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretSuffix"
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
@@ -58,7 +59,9 @@ const schema = yup.object({
|
||||
targetAppId: yup.string().required("GitLab project is required"),
|
||||
targetEnvironment: yup.string(),
|
||||
secretPrefix: yup.string(),
|
||||
secretSuffix: yup.string()
|
||||
secretSuffix: yup.string(),
|
||||
shouldMaskSecrets: yup.boolean(),
|
||||
shouldProtectSecrets: yup.boolean()
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof schema>;
|
||||
@@ -138,7 +141,9 @@ export default function GitLabCreateIntegrationPage() {
|
||||
targetAppId,
|
||||
targetEnvironment,
|
||||
secretPrefix,
|
||||
secretSuffix
|
||||
secretSuffix,
|
||||
shouldMaskSecrets,
|
||||
shouldProtectSecrets
|
||||
}: FormData) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -156,7 +161,9 @@ export default function GitLabCreateIntegrationPage() {
|
||||
secretPath,
|
||||
metadata: {
|
||||
secretPrefix,
|
||||
secretSuffix
|
||||
secretSuffix,
|
||||
shouldMaskSecrets,
|
||||
shouldProtectSecrets
|
||||
}
|
||||
});
|
||||
|
||||
@@ -390,6 +397,36 @@ export default function GitLabCreateIntegrationPage() {
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="pb-[14.25rem]"
|
||||
>
|
||||
<div className="ml-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldMaskSecrets"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="should-mask-secrets"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
<div className="max-w-md">Mark Infisical secrets in Gitlab as 'Masked' secrets</div>
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-1 mt-4 mb-5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldProtectSecrets"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="should-protect-secrets"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
Mark Infisical secrets in Gitlab as 'Protected' secrets
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPrefix"
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { useFetchServerStatus } from "@app/hooks/api";
|
||||
@@ -32,6 +34,9 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const { config } = useServerConfig();
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
const [captchaToken, setCaptchaToken] = useState("");
|
||||
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
||||
const captchaRef = useRef<HCaptcha>(null);
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -56,7 +61,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
// attemptCliLogin
|
||||
const isCliLoginSuccessful = await attemptCliLogin({
|
||||
email: email.toLowerCase(),
|
||||
password
|
||||
password,
|
||||
captchaToken
|
||||
});
|
||||
|
||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||
@@ -78,7 +84,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
} else {
|
||||
const isLoginSuccessful = await attemptLogin({
|
||||
email: email.toLowerCase(),
|
||||
password
|
||||
password,
|
||||
captchaToken
|
||||
});
|
||||
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
@@ -112,6 +119,12 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.response.data.error === "Captcha Required") {
|
||||
setShouldShowCaptcha(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginError(true);
|
||||
createNotification({
|
||||
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||
@@ -119,6 +132,11 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
});
|
||||
}
|
||||
|
||||
if (captchaRef.current) {
|
||||
captchaRef.current.resetCaptcha();
|
||||
}
|
||||
|
||||
setCaptchaToken("");
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
@@ -240,8 +258,19 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
className="select:-webkit-autofill:focus h-10"
|
||||
/>
|
||||
</div>
|
||||
{shouldShowCaptcha && (
|
||||
<div className="mt-4">
|
||||
<HCaptcha
|
||||
theme="dark"
|
||||
sitekey={CAPTCHA_SITE_KEY}
|
||||
onVerify={(token) => setCaptchaToken(token)}
|
||||
ref={captchaRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
disabled={shouldShowCaptcha && captchaToken === ""}
|
||||
type="submit"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import axios from "axios";
|
||||
import jwt_decode from "jwt-decode";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
||||
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||
@@ -41,6 +43,10 @@ export const PasswordStep = ({
|
||||
providerAuthToken
|
||||
) as any;
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState("");
|
||||
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
||||
const captchaRef = useRef<HCaptcha>(null);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
@@ -51,7 +57,8 @@ export const PasswordStep = ({
|
||||
const isCliLoginSuccessful = await attemptCliLogin({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
});
|
||||
|
||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||
@@ -99,7 +106,8 @@ export const PasswordStep = ({
|
||||
const loginAttempt = await attemptLogin({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
});
|
||||
|
||||
if (loginAttempt && loginAttempt.success) {
|
||||
@@ -158,11 +166,21 @@ export const PasswordStep = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.response.data.error === "Captcha Required") {
|
||||
setShouldShowCaptcha(true);
|
||||
return;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: "Login unsuccessful. Double-check your master password and try again.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
if (captchaRef.current) {
|
||||
captchaRef.current.resetCaptcha();
|
||||
}
|
||||
setCaptchaToken("");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -194,8 +212,19 @@ export const PasswordStep = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowCaptcha && (
|
||||
<div className="mx-auto mt-4 flex w-full min-w-[22rem] items-center justify-center lg:w-1/6">
|
||||
<HCaptcha
|
||||
theme="dark"
|
||||
sitekey={CAPTCHA_SITE_KEY}
|
||||
onVerify={(token) => setCaptchaToken(token)}
|
||||
ref={captchaRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[22rem] items-center justify-center rounded-md text-center lg:w-1/6">
|
||||
<Button
|
||||
disabled={shouldShowCaptcha && captchaToken === ""}
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
|
||||
+28
-10
@@ -45,7 +45,6 @@ export const computeImportedSecretRows = (
|
||||
if (importedSecIndex === -1) return [];
|
||||
|
||||
const importedSec = importSecrets[importedSecIndex];
|
||||
|
||||
const overridenSec: Record<string, { env: string; secretPath: string }> = {};
|
||||
|
||||
for (let i = importedSecIndex + 1; i < importSecrets.length; i += 1) {
|
||||
@@ -61,11 +60,28 @@ export const computeImportedSecretRows = (
|
||||
overridenSec[el.key] = { env: SECRET_IN_DASHBOARD, secretPath: "" };
|
||||
});
|
||||
|
||||
return importedSec.secrets.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
overriden: overridenSec?.[key]
|
||||
}));
|
||||
const importedEntry: Record<string, boolean> = {};
|
||||
const importedSecretEntries: {
|
||||
key: string;
|
||||
value: string;
|
||||
overriden: {
|
||||
env: string;
|
||||
secretPath: string;
|
||||
};
|
||||
}[] = [];
|
||||
|
||||
importedSec.secrets.forEach(({ key, value }) => {
|
||||
if (!importedEntry[key]) {
|
||||
importedSecretEntries.push({
|
||||
key,
|
||||
value,
|
||||
overriden: overridenSec?.[key]
|
||||
});
|
||||
importedEntry[key] = true;
|
||||
}
|
||||
});
|
||||
|
||||
return importedSecretEntries;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -159,8 +175,9 @@ export const SecretImportListView = ({
|
||||
importEnv.slug === environment &&
|
||||
isReserved &&
|
||||
importPath ===
|
||||
`${secretPath === "/" ? "" : secretPath}/${ReservedFolders.SecretReplication
|
||||
}${replicationImportId}`
|
||||
`${secretPath === "/" ? "" : secretPath}/${
|
||||
ReservedFolders.SecretReplication
|
||||
}${replicationImportId}`
|
||||
);
|
||||
if (reservedImport) {
|
||||
setReplicationSecrets((state) => ({
|
||||
@@ -206,8 +223,9 @@ export const SecretImportListView = ({
|
||||
isOpen={popUp.deleteSecretImport.isOpen}
|
||||
deleteKey="unlink"
|
||||
title="Do you want to remove this secret import?"
|
||||
subTitle={`This will unlink secrets from environment ${(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
|
||||
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
|
||||
subTitle={`This will unlink secrets from environment ${
|
||||
(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
|
||||
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||
onDeleteApproved={handleSecretImportDelete}
|
||||
/>
|
||||
|
||||
@@ -393,15 +393,15 @@ export const SecretDetailSidebar = ({
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="skipmultiencoding-option"
|
||||
onCheckedChange={(isChecked) => onChange(!isChecked)}
|
||||
isChecked={!value}
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
onBlur={onBlur}
|
||||
isDisabled={!isAllowed}
|
||||
className="items-center"
|
||||
>
|
||||
Enable multi line encoding
|
||||
Multi line encoding
|
||||
<Tooltip
|
||||
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
|
||||
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
|
||||
className="z-[100]"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
||||
|
||||
+9
-15
@@ -29,7 +29,7 @@ type Props = {
|
||||
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
|
||||
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
|
||||
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
|
||||
isImportedSecretPresentInEnv: (name: string, env: string, secretName: string) => boolean;
|
||||
isImportedSecretPresentInEnv: (env: string, secretName: string) => boolean;
|
||||
};
|
||||
|
||||
export const SecretOverviewTableRow = ({
|
||||
@@ -53,9 +53,8 @@ export const SecretOverviewTableRow = ({
|
||||
<>
|
||||
<Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group">
|
||||
<Td
|
||||
className={`sticky left-0 z-10 bg-mineshaft-800 bg-clip-padding py-0 px-0 group-hover:bg-mineshaft-700 ${
|
||||
isFormExpanded && "border-t-2 border-mineshaft-500"
|
||||
}`}
|
||||
className={`sticky left-0 z-10 bg-mineshaft-800 bg-clip-padding py-0 px-0 group-hover:bg-mineshaft-700 ${isFormExpanded && "border-t-2 border-mineshaft-500"
|
||||
}`}
|
||||
>
|
||||
<div className="h-full w-full border-r border-mineshaft-600 py-2.5 px-5">
|
||||
<div className="flex items-center space-x-5">
|
||||
@@ -83,7 +82,7 @@ export const SecretOverviewTableRow = ({
|
||||
{environments.map(({ slug }, i) => {
|
||||
const secret = getSecretByKey(slug, secretKey);
|
||||
|
||||
const isSecretImported = isImportedSecretPresentInEnv(secretPath, slug, secretKey);
|
||||
const isSecretImported = isImportedSecretPresentInEnv(slug, secretKey);
|
||||
|
||||
const isSecretPresent = Boolean(secret);
|
||||
const isSecretEmpty = secret?.value === "";
|
||||
@@ -108,8 +107,8 @@ export const SecretOverviewTableRow = ({
|
||||
isSecretPresent
|
||||
? "Present secret"
|
||||
: isSecretImported
|
||||
? "Imported secret"
|
||||
: "Missing secret"
|
||||
? "Imported secret"
|
||||
: "Missing secret"
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
@@ -133,9 +132,8 @@ export const SecretOverviewTableRow = ({
|
||||
<Tr>
|
||||
<Td
|
||||
colSpan={totalCols}
|
||||
className={`bg-bunker-600 px-0 py-0 ${
|
||||
isFormExpanded && "border-b-2 border-mineshaft-500"
|
||||
}`}
|
||||
className={`bg-bunker-600 px-0 py-0 ${isFormExpanded && "border-b-2 border-mineshaft-500"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="ml-2 p-2"
|
||||
@@ -180,11 +178,7 @@ export const SecretOverviewTableRow = ({
|
||||
const secret = getSecretByKey(slug, secretKey);
|
||||
const isCreatable = !secret;
|
||||
|
||||
const isImportedSecret = isImportedSecretPresentInEnv(
|
||||
secretPath,
|
||||
slug,
|
||||
secretKey
|
||||
);
|
||||
const isImportedSecret = isImportedSecretPresentInEnv(slug, secretKey);
|
||||
|
||||
return (
|
||||
<tr
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useProjectPermission, useWorkspace } from "@app/context";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { useUpdateWorkspaceVersionLimit } from "@app/hooks/api/workspace/queries";
|
||||
|
||||
const formSchema = z.object({
|
||||
pitVersionLimit: z.coerce.number().min(1).max(100)
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
export const PointInTimeVersionLimitSection = () => {
|
||||
const { mutateAsync: updatePitVersion } = useUpdateWorkspaceVersionLimit();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { membership } = useProjectPermission();
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
pitVersionLimit: currentWorkspace?.pitVersionLimit || 10
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentWorkspace) return null;
|
||||
|
||||
const handleVersionLimitSubmit = async ({ pitVersionLimit }: TForm) => {
|
||||
try {
|
||||
await updatePitVersion({
|
||||
pitVersionLimit,
|
||||
projectSlug: currentWorkspace.slug
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated version limit",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed updating project's version limit",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-semibold">Version Retention</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
|
||||
This defines the maximum number of recent secret versions to keep per folder. Excess versions will be removed at midnight (UTC) each day.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(handleVersionLimitSubmit)} autoComplete="off">
|
||||
<div className="max-w-xs">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={0}
|
||||
name="pitVersionLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Recent versions to keep"
|
||||
>
|
||||
<Input {...field} type="number" min={1} step={1} isDisabled={!isAdmin} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isAdmin || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PointInTimeVersionLimitSection } from "./PointInTimeVersionLimitSection";
|
||||
+2
@@ -3,6 +3,7 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { E2EESection } from "../E2EESection";
|
||||
import { EnvironmentSection } from "../EnvironmentSection";
|
||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||
import { ProjectNameChangeSection } from "../ProjectNameChangeSection";
|
||||
import { SecretTagsSection } from "../SecretTagsSection";
|
||||
|
||||
@@ -14,6 +15,7 @@ export const ProjectGeneralTab = () => {
|
||||
<SecretTagsSection />
|
||||
<AutoCapitalizationSection />
|
||||
<E2EESection />
|
||||
<PointInTimeVersionLimitSection />
|
||||
<BackfillSecretReferenceSecretion />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user