mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 04:14:38 +00:00
Merge remote-tracking branch 'origin/main' into shubham/eng-993-allow-self-hosted-users-to-set-their-own-rate-limits-in
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);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
TUpdateWorkspaceIdentityRoleDTO,
|
||||
TUpdateWorkspaceUserRoleDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
UpdatePitVersionLimitDTO,
|
||||
Workspace
|
||||
} from "./types";
|
||||
|
||||
@@ -249,6 +250,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();
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ export type Workspace = {
|
||||
orgId: string;
|
||||
version: ProjectVersion;
|
||||
upgradeStatus: string | null;
|
||||
updatedAt: string;
|
||||
autoCapitalization: boolean;
|
||||
environments: WorkspaceEnv[];
|
||||
pitVersionLimit: number;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
@@ -48,6 +50,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 +131,4 @@ export type TUpdateWorkspaceGroupRoleDTO = {
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
)[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
export const timeAgo = (inputDate: Date, currentDate: Date): string => {
|
||||
const now = new Date(currentDate).getTime();
|
||||
const date = new Date(inputDate).getTime();
|
||||
const elapsedMilliseconds = now - date;
|
||||
const elapsedSeconds = Math.abs(Math.floor(elapsedMilliseconds / 1000));
|
||||
const elapsedMinutes = Math.abs(Math.floor(elapsedSeconds / 60));
|
||||
const elapsedHours = Math.abs(Math.floor(elapsedMinutes / 60));
|
||||
const elapsedDays = Math.abs(Math.floor(elapsedHours / 24));
|
||||
const elapsedWeeks = Math.abs(Math.floor(elapsedDays / 7));
|
||||
const elapsedMonths = Math.abs(Math.floor(elapsedDays / 30));
|
||||
const elapsedYears = Math.abs(Math.floor(elapsedDays / 365));
|
||||
|
||||
if (elapsedYears > 0) {
|
||||
return `${elapsedYears} year${elapsedYears === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedMonths > 0) {
|
||||
return `${elapsedMonths} month${elapsedMonths === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedWeeks > 0) {
|
||||
return `${elapsedWeeks} week${elapsedWeeks === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedDays > 0) {
|
||||
return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedHours > 0) {
|
||||
return `${elapsedHours} hour${elapsedHours === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedMinutes > 0) {
|
||||
return `${elapsedMinutes} minute${elapsedMinutes === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
return `${elapsedSeconds} second${elapsedSeconds === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -12,11 +12,14 @@ import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRight,
|
||||
faArrowUpRightFromSquare,
|
||||
faBorderAll,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faClipboard,
|
||||
faExclamationCircle,
|
||||
faFileShield,
|
||||
faHandPeace,
|
||||
faList,
|
||||
faMagnifyingGlass,
|
||||
faNetworkWired,
|
||||
faPlug,
|
||||
@@ -35,6 +38,7 @@ import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -86,6 +90,11 @@ type ItemProps = {
|
||||
link?: string;
|
||||
};
|
||||
|
||||
enum ProjectsViewMode {
|
||||
GRID = "grid",
|
||||
LIST = "list"
|
||||
}
|
||||
|
||||
function copyToClipboard(id: string, setState: (value: boolean) => void) {
|
||||
// Get the text field
|
||||
const copyText = document.getElementById(id) as HTMLInputElement;
|
||||
@@ -309,8 +318,9 @@ const LearningItem = ({
|
||||
href={link}
|
||||
>
|
||||
<div
|
||||
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||
} mb-3 rounded-md`}
|
||||
className={`${
|
||||
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||
} mb-3 rounded-md`}
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@@ -321,10 +331,11 @@ const LearningItem = ({
|
||||
await registerUserAction.mutateAsync(userAction);
|
||||
}
|
||||
}}
|
||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete
|
||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
|
||||
complete
|
||||
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
||||
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
|
||||
} text-mineshaft-100 duration-200`}
|
||||
} text-mineshaft-100 duration-200`}
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
@@ -402,8 +413,9 @@ const LearningItemSquare = ({
|
||||
href={link}
|
||||
>
|
||||
<div
|
||||
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||
} w-full rounded-md`}
|
||||
className={`${
|
||||
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||
} w-full rounded-md`}
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@@ -414,10 +426,11 @@ const LearningItemSquare = ({
|
||||
await registerUserAction.mutateAsync(userAction);
|
||||
}
|
||||
}}
|
||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete
|
||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${
|
||||
complete
|
||||
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
||||
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
|
||||
} text-mineshaft-100 duration-200`}
|
||||
} text-mineshaft-100 duration-200`}
|
||||
>
|
||||
<div className="flex w-full flex-col items-center px-6 py-4">
|
||||
<div className="flex w-full flex-row items-start justify-between">
|
||||
@@ -431,8 +444,9 @@ const LearningItemSquare = ({
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "font-semibold text-primary" : ""
|
||||
}`}
|
||||
className={`text-right text-sm font-normal text-mineshaft-300 ${
|
||||
complete ? "font-semibold text-primary" : ""
|
||||
}`}
|
||||
>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
@@ -461,7 +475,6 @@ const formSchema = yup.object({
|
||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
// #TODO: Update all the workspaceIds
|
||||
|
||||
const OrganizationPage = withPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
@@ -496,6 +509,9 @@ const OrganizationPage = withPermission(
|
||||
const createWs = useCreateWorkspace();
|
||||
const { user } = useUser();
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
|
||||
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
|
||||
);
|
||||
|
||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||
// type check
|
||||
@@ -550,6 +566,95 @@ const OrganizationPage = withPermission(
|
||||
}, []);
|
||||
|
||||
const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0;
|
||||
const filteredWorkspaces = orgWorkspaces.filter((ws) =>
|
||||
ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
);
|
||||
|
||||
const projectsGridView = (
|
||||
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{isWorkspaceLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="mt-0 text-lg text-mineshaft-100">
|
||||
<Skeleton className="w-3/4 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredWorkspaces.map((workspace) => (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
onClick={() => {
|
||||
router.push(`/project/${workspace.id}/secrets/overview`);
|
||||
localStorage.setItem("projectData.id", workspace.id);
|
||||
}}
|
||||
key={workspace.id}
|
||||
className="min-w-72 group flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||
{workspace.environments?.length || 0} environments
|
||||
</div>
|
||||
<button type="button">
|
||||
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all group-hover:border-primary-500/80 group-hover:bg-primary-800/20 group-hover:text-mineshaft-200">
|
||||
Explore{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const projectsListView = (
|
||||
<div className="mt-4 w-full rounded-md">
|
||||
{isWorkspaceLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className={`min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||
i === 0 && "rounded-t-md"
|
||||
} ${i === 2 && "rounded-b-md border-b"}`}
|
||||
>
|
||||
<Skeleton className="w-full bg-mineshaft-600" />
|
||||
</div>
|
||||
))}
|
||||
{filteredWorkspaces.map((workspace, ind) => (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
onClick={() => {
|
||||
router.push(`/project/${workspace.id}/secrets/overview`);
|
||||
localStorage.setItem("projectData.id", workspace.id);
|
||||
}}
|
||||
key={workspace.id}
|
||||
className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||
ind === 0 && "rounded-t-md"
|
||||
} ${ind === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`}
|
||||
>
|
||||
<div className="flex items-center sm:col-span-3 lg:col-span-4">
|
||||
<FontAwesomeIcon icon={faFileShield} className="text-sm text-primary/70" />
|
||||
<div className="ml-5 truncate text-sm text-mineshaft-100">{workspace.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
|
||||
<div className="text-center text-sm text-mineshaft-300">
|
||||
{workspace.environments?.length || 0} environments
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
|
||||
@@ -580,7 +685,9 @@ const OrganizationPage = withPermission(
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||
<div className="flex w-full justify-between">
|
||||
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||
</div>
|
||||
<div className="mt-6 flex w-full flex-row">
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
@@ -589,6 +696,36 @@ const OrganizationPage = withPermission(
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID);
|
||||
setProjectsViewMode(ProjectsViewMode.GRID);
|
||||
}}
|
||||
ariaLabel="grid"
|
||||
size="xs"
|
||||
className={`${
|
||||
projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBorderAll} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST);
|
||||
setProjectsViewMode(ProjectsViewMode.LIST);
|
||||
}}
|
||||
ariaLabel="list"
|
||||
size="xs"
|
||||
className={`${
|
||||
projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faList} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
@@ -609,52 +746,7 @@ const OrganizationPage = withPermission(
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{isWorkspaceLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="mt-0 text-lg text-mineshaft-100">
|
||||
<Skeleton className="w-3/4 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{orgWorkspaces
|
||||
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||
.map((workspace) => (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
onClick={() => {
|
||||
router.push(`/project/${workspace.id}/secrets/overview`);
|
||||
localStorage.setItem("projectData.id", workspace.id);
|
||||
}}
|
||||
key={workspace.id}
|
||||
className="min-w-72 group flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||
{workspace.environments?.length || 0} environments
|
||||
</div>
|
||||
<button type="button">
|
||||
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all group-hover:border-primary-500/80 group-hover:bg-primary-800/20 group-hover:text-mineshaft-200">
|
||||
Explore{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{projectsViewMode === ProjectsViewMode.LIST ? projectsListView : projectsGridView}
|
||||
{isWorkspaceEmpty && (
|
||||
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||
<FontAwesomeIcon
|
||||
@@ -709,94 +801,95 @@ const OrganizationPage = withPermission(
|
||||
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
||||
30 * 24 * 60 * 60 * 1000
|
||||
) && (
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
||||
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<LearningItemSquare
|
||||
text="Watch Infisical demo"
|
||||
subText="Set up Infisical in 3 min."
|
||||
complete={hasUserClickedIntro}
|
||||
icon={faHandPeace}
|
||||
time="3 min"
|
||||
userAction="intro_cta_clicked"
|
||||
link="https://www.youtube.com/watch?v=PK23097-25I"
|
||||
/>
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<>
|
||||
<LearningItemSquare
|
||||
text="Add your secrets"
|
||||
subText="Drop a .env file or type your secrets."
|
||||
complete={hasUserPushedSecrets}
|
||||
icon={faPlus}
|
||||
time="1 min"
|
||||
userAction="first_time_secrets_pushed"
|
||||
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
|
||||
/>
|
||||
<LearningItemSquare
|
||||
text="Invite your teammates"
|
||||
subText="Infisical is better used as a team."
|
||||
complete={usersInOrg}
|
||||
icon={faUserPlus}
|
||||
time="2 min"
|
||||
link={`/org/${router.query.id}/members?action=invite`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="block xl:hidden 2xl:block">
|
||||
<LearningItemSquare
|
||||
text="Join Infisical Slack"
|
||||
subText="Have any questions? Ask us!"
|
||||
complete={hasUserClickedSlack}
|
||||
icon={faSlack}
|
||||
time="1 min"
|
||||
userAction="slack_cta_clicked"
|
||||
link="https://infisical.com/slack"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
||||
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<LearningItemSquare
|
||||
text="Watch Infisical demo"
|
||||
subText="Set up Infisical in 3 min."
|
||||
complete={hasUserClickedIntro}
|
||||
icon={faHandPeace}
|
||||
time="3 min"
|
||||
userAction="intro_cta_clicked"
|
||||
link="https://www.youtube.com/watch?v=PK23097-25I"
|
||||
/>
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
|
||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||
<div className="mr-4 flex w-full flex-row items-center">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||
{false && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="h-5 w-5 text-4xl text-green"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start pl-0.5">
|
||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">
|
||||
Replace .env files with a more secure and efficient alternative.
|
||||
</div>
|
||||
<>
|
||||
<LearningItemSquare
|
||||
text="Add your secrets"
|
||||
subText="Drop a .env file or type your secrets."
|
||||
complete={hasUserPushedSecrets}
|
||||
icon={faPlus}
|
||||
time="1 min"
|
||||
userAction="first_time_secrets_pushed"
|
||||
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
|
||||
/>
|
||||
<LearningItemSquare
|
||||
text="Invite your teammates"
|
||||
subText="Infisical is better used as a team."
|
||||
complete={usersInOrg}
|
||||
icon={faUserPlus}
|
||||
time="2 min"
|
||||
link={`/org/${router.query.id}/members?action=invite`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="block xl:hidden 2xl:block">
|
||||
<LearningItemSquare
|
||||
text="Join Infisical Slack"
|
||||
subText="Have any questions? Ask us!"
|
||||
complete={hasUserClickedSlack}
|
||||
icon={faSlack}
|
||||
time="1 min"
|
||||
userAction="slack_cta_clicked"
|
||||
link="https://infisical.com/slack"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
|
||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||
<div className="mr-4 flex w-full flex-row items-center">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||
{false && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="h-5 w-5 text-4xl text-green"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start pl-0.5">
|
||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">
|
||||
Replace .env files with a more secure and efficient alternative.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"
|
||||
}`}
|
||||
>
|
||||
About 2 min
|
||||
</div>
|
||||
</div>
|
||||
<TabsObject />
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
<div
|
||||
className={`w-28 pr-4 text-right text-sm font-semibold ${
|
||||
false && "text-green"
|
||||
}`}
|
||||
>
|
||||
About 2 min
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<LearningItem
|
||||
text="Integrate Infisical with your infrastructure"
|
||||
subText="Connect Infisical to various 3rd party services and platforms."
|
||||
complete={false}
|
||||
icon={faPlug}
|
||||
time="15 min"
|
||||
link="https://infisical.com/docs/integrations/overview"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<TabsObject />
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
</div>
|
||||
)}
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<LearningItem
|
||||
text="Integrate Infisical with your infrastructure"
|
||||
subText="Connect Infisical to various 3rd party services and platforms."
|
||||
complete={false}
|
||||
icon={faPlug}
|
||||
time="15 min"
|
||||
link="https://infisical.com/docs/integrations/overview"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isModalOpen) => {
|
||||
|
||||
@@ -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