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:
Sheen Capadngan
2024-06-13 15:44:50 +08:00
92 changed files with 2041 additions and 508 deletions
@@ -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);
+23 -21
View File
@@ -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);
}
+1
View File
@@ -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();
+4 -1
View File
@@ -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;
}
)[];
};
};
+46
View File
@@ -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 &apos;Masked&apos; 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 &apos;Protected&apos; secrets
</Switch>
)}
/>
</div>
<Controller
control={control}
name="secretPrefix"
+234 -141
View File
@@ -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"
@@ -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" />
@@ -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
@@ -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>
);
};
@@ -0,0 +1 @@
export { PointInTimeVersionLimitSection } from "./PointInTimeVersionLimitSection";
@@ -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>