mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 22:29:33 +00:00
Merge branch 'main' into feat-admin-signup
This commit is contained in:
@@ -59,8 +59,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
||||
const workspaceId = router.query.id as string;
|
||||
// Delete the row in the table (e.g. a user)
|
||||
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
|
||||
const handleDelete = async (membershipId: string) => {
|
||||
await deleteUserFromWorkspaceMutateAsync({ membershipId, workspaceId });
|
||||
const handleDelete = async (email: string) => {
|
||||
await deleteUserFromWorkspaceMutateAsync({ emails: [email], workspaceId });
|
||||
};
|
||||
|
||||
const handleRoleUpdate = async (index: number, e: string) => {
|
||||
@@ -368,7 +368,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
||||
myRole !== "member" ? (
|
||||
<div className="mt-0.5 flex items-center opacity-50 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() => handleDelete(row.membershipId)}
|
||||
onButtonPressed={() => handleDelete(row.email)}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
|
||||
@@ -13,12 +13,12 @@ nacl.util = require("tweetnacl-util");
|
||||
*/
|
||||
const generateKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
return ({
|
||||
publicKey: nacl.util.encodeBase64(pair.publicKey),
|
||||
privateKey: nacl.util.encodeBase64(pair.secretKey)
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
publicKey: nacl.util.encodeBase64(pair.publicKey),
|
||||
privateKey: nacl.util.encodeBase64(pair.secretKey)
|
||||
};
|
||||
};
|
||||
|
||||
type EncryptAsymmetricProps = {
|
||||
plaintext: string;
|
||||
@@ -29,27 +29,19 @@ type EncryptAsymmetricProps = {
|
||||
/**
|
||||
* Verify that private key [privateKey] is the one that corresponds to
|
||||
* the public key [publicKey]
|
||||
* @param {Object}
|
||||
* @param {Object}
|
||||
* @param {String} - base64-encoded Nacl private key
|
||||
* @param {String} - base64-encoded Nacl public key
|
||||
*/
|
||||
const verifyPrivateKey = ({
|
||||
privateKey,
|
||||
publicKey
|
||||
}: {
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
}) => {
|
||||
const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => {
|
||||
const derivedPublicKey = nacl.util.encodeBase64(
|
||||
nacl.box.keyPair.fromSecretKey(
|
||||
nacl.util.decodeBase64(privateKey)
|
||||
).publicKey
|
||||
nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(privateKey)).publicKey
|
||||
);
|
||||
|
||||
|
||||
if (derivedPublicKey !== publicKey) {
|
||||
throw new Error("Failed to verify private key");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive a key from password [password] and salt [salt] using Argon2id
|
||||
@@ -229,7 +221,8 @@ export {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric,
|
||||
deriveArgonKey,
|
||||
encryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
generateKeyPair,
|
||||
verifyPrivateKey};
|
||||
verifyPrivateKey
|
||||
};
|
||||
|
||||
@@ -56,7 +56,6 @@ const encryptSecrets = async ({
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
} else {
|
||||
// case: a (shared) key does not exist for the workspace
|
||||
randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
@@ -116,7 +115,6 @@ const encryptSecrets = async ({
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error while encrypting secrets");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Spinner } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetUpgradeProjectStatus } from "@app/hooks/api/workspace/queries";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const UpgradeOverlay = () => {
|
||||
const router = useRouter();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const [isUpgrading, setIsUpgrading] = useToggle(false);
|
||||
|
||||
const isProjectRoute = router.pathname.includes("/project");
|
||||
|
||||
const { isLoading: isUpgradeStatusLoading } = useGetUpgradeProjectStatus({
|
||||
projectId: currentWorkspace?.id ?? "",
|
||||
enabled: isProjectRoute && currentWorkspace && currentWorkspace.version === ProjectVersion.V1,
|
||||
refetchInterval: 5_000,
|
||||
onSuccess: (data) => {
|
||||
if (!data) return;
|
||||
|
||||
if (data.status !== "IN_PROGRESS") {
|
||||
setIsUpgrading.off();
|
||||
} else if (data?.status === "IN_PROGRESS") {
|
||||
setIsUpgrading.on();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// make sure only to display this on /project routes
|
||||
if (!currentWorkspace || !isProjectRoute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// for non admin this would throw an error
|
||||
// so no need to render
|
||||
return !isUpgradeStatusLoading && isUpgrading ? ( // isUpgrading
|
||||
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
|
||||
<Spinner size="lg" className="text-primary" />
|
||||
<div className="ml-4 flex flex-col space-y-1">
|
||||
<div className="text-3xl font-medium text-white">Please wait</div>
|
||||
<span className="inline-block text-white">Upgrading your project...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { UpgradeOverlay } from "./UpgradeOverlay";
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { useProjectPermission } from "@app/context";
|
||||
import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
|
||||
import { Workspace } from "@app/hooks/api/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { Button } from "../Button";
|
||||
|
||||
export type UpgradeProjectAlertProps = {
|
||||
project: Workspace;
|
||||
};
|
||||
|
||||
export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.Element | null => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const router = useRouter();
|
||||
const { membership } = useProjectPermission();
|
||||
const upgradeProject = useUpgradeProject();
|
||||
const [currentStatus, setCurrentStatus] = useState<string | null>(null);
|
||||
const [isUpgrading, setIsUpgrading] = useState(false);
|
||||
|
||||
const {
|
||||
data: projectStatus,
|
||||
isLoading: statusIsLoading,
|
||||
refetch: manualProjectStatusRefetch
|
||||
} = useGetUpgradeProjectStatus({
|
||||
projectId: project.id,
|
||||
enabled: membership.role === "admin" && project.version === ProjectVersion.V1,
|
||||
refetchInterval: 5_000,
|
||||
onSuccess: (data) => {
|
||||
if (membership.role !== "admin") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data && data?.status !== null) {
|
||||
if (data.status === "IN_PROGRESS") {
|
||||
setCurrentStatus("Your upgrade is being processed.");
|
||||
} else if (data.status === "FAILED") {
|
||||
setCurrentStatus("Upgrade failed, please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStatus !== null && data?.status === null) {
|
||||
router.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onUpgradeProject = useCallback(async () => {
|
||||
if (upgradeProject.isLoading) {
|
||||
return;
|
||||
}
|
||||
setIsUpgrading(true);
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
if (!PRIVATE_KEY) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Private key not found"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await upgradeProject.mutateAsync({
|
||||
projectId: project.id,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
manualProjectStatusRefetch();
|
||||
|
||||
setTimeout(() => setIsUpgrading(false), 5_000);
|
||||
}, []);
|
||||
|
||||
const isLoading =
|
||||
isUpgrading ||
|
||||
((upgradeProject.isLoading ||
|
||||
currentStatus !== null ||
|
||||
(currentStatus === null && statusIsLoading)) &&
|
||||
projectStatus?.status !== "FAILED");
|
||||
|
||||
if (project.version !== ProjectVersion.V1) return null;
|
||||
if (membership.role !== "admin") return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
|
||||
<FontAwesomeIcon icon={faWarning} className="pr-6 text-6xl text-white/80" />
|
||||
<div className="flex w-full flex-col text-sm">
|
||||
<span className="mb-2 text-lg font-semibold">Upgrade your project</span>
|
||||
Upgrade your project version to continue receiving the latest improvements and patches.
|
||||
{currentStatus && <p className="mt-2 opacity-80">Status: {currentStatus}</p>}
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Button isLoading={isLoading} isDisabled={isLoading} onClick={onUpgradeProject}>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { UpgradeProjectAlert } from "./UpgradeProjectAlert";
|
||||
@@ -13,6 +13,7 @@ export enum OrgPermissionSubjects {
|
||||
Member = "member",
|
||||
Settings = "settings",
|
||||
IncidentAccount = "incident-contact",
|
||||
Scim = "scim",
|
||||
Sso = "sso",
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
@@ -26,6 +27,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Member]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets";
|
||||
import { uploadWsKey } from "@app/hooks/api/keys/queries";
|
||||
import { createSecret } from "@app/hooks/api/secrets/mutations";
|
||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
||||
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
||||
|
||||
const secretsToBeAdded = [
|
||||
@@ -91,43 +86,22 @@ const initProjectHelper = async ({
|
||||
}) => {
|
||||
// create new project
|
||||
const {
|
||||
data: { workspace }
|
||||
data: { project }
|
||||
} = await createWorkspace({
|
||||
workspaceName: projectName,
|
||||
organizationId
|
||||
});
|
||||
|
||||
// create and upload new (encrypted) project key
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
if (!PRIVATE_KEY) throw new Error("Failed to find private key");
|
||||
|
||||
const user = await fetchUserDetails();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadWsKey({
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
organizationId,
|
||||
projectName
|
||||
});
|
||||
|
||||
// encrypt and upload secrets to new project
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeAdded,
|
||||
workspaceId: workspace.id,
|
||||
workspaceId: project.id,
|
||||
env: "dev"
|
||||
});
|
||||
|
||||
secrets?.forEach((secret) => {
|
||||
createSecret({
|
||||
workspaceId: workspace.id,
|
||||
workspaceId: project.id,
|
||||
environment: secret.environment,
|
||||
type: secret.type,
|
||||
secretKey: secret.secretName,
|
||||
@@ -147,7 +121,7 @@ const initProjectHelper = async ({
|
||||
});
|
||||
});
|
||||
|
||||
return workspace;
|
||||
return project;
|
||||
};
|
||||
|
||||
export { initProjectHelper };
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { organizationKeys } from "../organization/queries";
|
||||
import { User } from "../users/types";
|
||||
import { adminQueryKeys } from "./queries";
|
||||
import { TCreateAdminUserDTO, TServerConfig } from "./types";
|
||||
@@ -9,7 +10,11 @@ import { TCreateAdminUserDTO, TServerConfig } from "./types";
|
||||
export const useCreateAdminUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ user: User; token: string }, {}, TCreateAdminUserDTO>({
|
||||
return useMutation<
|
||||
{ user: User; token: string; organization: { id: string } },
|
||||
{},
|
||||
TCreateAdminUserDTO
|
||||
>({
|
||||
mutationFn: async (opt) => {
|
||||
const { data } = await apiRequest.post("/api/v1/admin/signup", opt);
|
||||
return data;
|
||||
@@ -34,6 +39,7 @@ export const useUpdateServerConfig = () => {
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
|
||||
queryClient.invalidateQueries(adminQueryKeys.serverConfig());
|
||||
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./roles";
|
||||
export * from "./scim";
|
||||
export * from "./secretApproval";
|
||||
export * from "./secretApprovalRequest";
|
||||
export * from "./secretFolders";
|
||||
|
||||
@@ -8,9 +8,9 @@ const encKeyKeys = {
|
||||
getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const
|
||||
};
|
||||
|
||||
export const fetchUserWsKey = async (workspaceID: string) => {
|
||||
export const fetchUserWsKey = async (projectId: string) => {
|
||||
const { data } = await apiRequest.get<UserWsKeyPair>(
|
||||
`/api/v2/workspace/${workspaceID}/encrypted-key`
|
||||
`/api/v2/workspace/${projectId}/encrypted-key`
|
||||
);
|
||||
|
||||
return data;
|
||||
|
||||
@@ -71,12 +71,14 @@ export const useUpdateOrg = () => {
|
||||
mutationFn: ({
|
||||
name,
|
||||
authEnforced,
|
||||
scimEnabled,
|
||||
slug,
|
||||
orgId
|
||||
}) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
authEnforced,
|
||||
scimEnabled,
|
||||
slug
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ export type Organization = {
|
||||
createAt: string;
|
||||
updatedAt: string;
|
||||
authEnforced: boolean;
|
||||
scimEnabled: boolean;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
@@ -11,6 +12,7 @@ export type UpdateOrgDTO = {
|
||||
orgId: string;
|
||||
name?: string;
|
||||
authEnforced?: boolean;
|
||||
scimEnabled?: boolean;
|
||||
slug?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
useCreateScimToken,
|
||||
useDeleteScimToken
|
||||
} from "./mutations";
|
||||
export { useGetScimTokens } from "./queries";
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { scimKeys } from "./queries";
|
||||
import {
|
||||
CreateScimTokenDTO,
|
||||
CreateScimTokenRes,
|
||||
DeleteScimTokenDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateScimToken = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<CreateScimTokenRes, {}, CreateScimTokenDTO>({
|
||||
mutationFn: async ({
|
||||
organizationId,
|
||||
description,
|
||||
ttlDays
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/scim/scim-tokens", {
|
||||
organizationId,
|
||||
description,
|
||||
ttlDays
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(scimKeys.getScimTokens(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteScimToken = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<CreateScimTokenRes, {}, DeleteScimTokenDTO>({
|
||||
mutationFn: async ({ scimTokenId }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/scim/scim-tokens/${scimTokenId}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(scimKeys.getScimTokens(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { ScimTokenData } from "./types";
|
||||
|
||||
export const scimKeys = {
|
||||
getScimTokens: (orgId: string) => [{ orgId }, "organization-scim-token"] as const,
|
||||
};
|
||||
|
||||
export const useGetScimTokens = (organizationId: string) => {
|
||||
return useQuery({
|
||||
queryKey: scimKeys.getScimTokens(organizationId),
|
||||
queryFn: async () => {
|
||||
if (organizationId === "") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { data: { scimTokens } } = await apiRequest.get<{ scimTokens: ScimTokenData[] }>(`/api/v1/scim/scim-tokens?organizationId=${organizationId}`);
|
||||
|
||||
return scimTokens;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
export type ScimTokenData = {
|
||||
id: string;
|
||||
ttlDays: number;
|
||||
description: string;
|
||||
tokenSuffix: string;
|
||||
orgId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type CreateScimTokenDTO = {
|
||||
organizationId: string;
|
||||
description?: string;
|
||||
ttlDays?: number;
|
||||
}
|
||||
|
||||
export type DeleteScimTokenDTO = {
|
||||
organizationId: string;
|
||||
scimTokenId: string;
|
||||
}
|
||||
|
||||
export type CreateScimTokenRes = {
|
||||
scimToken: string;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { organizationKeys } from "@app/hooks/api/organization/queries";
|
||||
|
||||
const ssoConfigKeys = {
|
||||
getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const
|
||||
@@ -82,8 +83,12 @@ export const useUpdateSSOConfig = () => {
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId));
|
||||
onSuccess(_, { organizationId, isActive }) {
|
||||
if (isActive === false) {
|
||||
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export type SubscriptionPlan = {
|
||||
workspacesUsed: number;
|
||||
environmentLimit: number;
|
||||
samlSSO: boolean;
|
||||
scim: boolean;
|
||||
status:
|
||||
| "incomplete"
|
||||
| "incomplete_expired"
|
||||
|
||||
@@ -22,7 +22,7 @@ export * from "./secrets/types";
|
||||
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
||||
export type { SubscriptionPlan } from "./subscriptions/types";
|
||||
export type { WsTag } from "./tags/types";
|
||||
export type { AddUserToWsDTO, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
|
||||
export type { AddUserToWsDTOE2EE, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
|
||||
export type { TWebhook } from "./webhooks/types";
|
||||
export type {
|
||||
CreateEnvironmentDTO,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { useAddUserToWs } from "./mutation";
|
||||
export { useAddUserToWsE2EE, useAddUserToWsNonE2EE } from "./mutation";
|
||||
export {
|
||||
fetchOrgUsers,
|
||||
useAddUserToOrg,
|
||||
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { AddUserToWsDTO } from "./types";
|
||||
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
|
||||
|
||||
export const useAddUserToWs = () => {
|
||||
export const useAddUserToWsE2EE = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, AddUserToWsDTO>({
|
||||
return useMutation<{}, {}, AddUserToWsDTOE2EE>({
|
||||
mutationFn: async ({ workspaceId, members, decryptKey, userPrivateKey }) => {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
@@ -45,3 +45,19 @@ export const useAddUserToWs = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddUserToWsNonE2EE = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
|
||||
mutationFn: async ({ projectId, emails }) => {
|
||||
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
|
||||
emails
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ export type TProjectMembership = {
|
||||
|
||||
export type TWorkspaceUser = OrgUser;
|
||||
|
||||
export type AddUserToWsDTO = {
|
||||
export type AddUserToWsDTOE2EE = {
|
||||
workspaceId: string;
|
||||
decryptKey: UserWsKeyPair;
|
||||
userPrivateKey: string;
|
||||
@@ -73,6 +73,11 @@ export type AddUserToWsDTO = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type AddUserToWsDTONonE2EE = {
|
||||
projectId: string;
|
||||
emails: string[];
|
||||
};
|
||||
|
||||
export type UpdateOrgUserRoleDTO = {
|
||||
organizationId: string;
|
||||
membershipId: string;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export {
|
||||
useAddIdentityToWorkspace,
|
||||
useAddUserToWorkspace,
|
||||
useCreateWorkspace,
|
||||
useCreateWsEnvironment,
|
||||
useDeleteIdentityFromWorkspace,
|
||||
useDeleteUserFromWorkspace,
|
||||
useDeleteWorkspace,
|
||||
useDeleteWsEnvironment,
|
||||
useGetUpgradeProjectStatus,
|
||||
useGetUserWorkspaceMemberships,
|
||||
useGetUserWorkspaces,
|
||||
useGetWorkspaceAuthorizations,
|
||||
@@ -21,5 +21,6 @@ export {
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateIdentityWorkspaceRole,
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUpdateWsEnvironment
|
||||
useUpdateWsEnvironment,
|
||||
useUpgradeProject
|
||||
} from "./queries";
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DeleteWorkspaceDTO,
|
||||
NameWorkspaceSecretsDTO,
|
||||
RenameWorkspaceDTO,
|
||||
TGetUpgradeProjectStatusDTO,
|
||||
ToggleAutoCapitalizationDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
Workspace
|
||||
@@ -24,6 +25,7 @@ export const workspaceKeys = {
|
||||
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
|
||||
getWorkspaceIndexStatus: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-index-status"] as const,
|
||||
getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"],
|
||||
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||
@@ -51,6 +53,14 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const fetchProjectUpgradeStatus = async (projectId: string) => {
|
||||
const { data } = await apiRequest.get<{ status: string }>(
|
||||
`/api/v2/workspace/${projectId}/upgrade/status`
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchWorkspaceSecrets = async (workspaceId: string) => {
|
||||
const {
|
||||
data: { secrets }
|
||||
@@ -61,6 +71,36 @@ export const fetchWorkspaceSecrets = async (workspaceId: string) => {
|
||||
return secrets;
|
||||
};
|
||||
|
||||
export const useUpgradeProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, { projectId: string; privateKey: string }>({
|
||||
mutationFn: ({ projectId, privateKey }) => {
|
||||
return apiRequest.post(`/api/v2/workspace/${projectId}/upgrade`, {
|
||||
userPrivateKey: privateKey
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUpgradeProjectStatus = ({
|
||||
projectId,
|
||||
onSuccess,
|
||||
enabled,
|
||||
refetchInterval
|
||||
}: TGetUpgradeProjectStatusDTO) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getProjectUpgradeStatus(projectId),
|
||||
queryFn: () => fetchProjectUpgradeStatus(projectId),
|
||||
enabled,
|
||||
onSuccess,
|
||||
refetchInterval
|
||||
});
|
||||
};
|
||||
|
||||
const fetchUserWorkspaces = async () => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||
return data.workspaces;
|
||||
@@ -158,19 +198,19 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
||||
|
||||
export const createWorkspace = ({
|
||||
organizationId,
|
||||
workspaceName
|
||||
}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => {
|
||||
return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId });
|
||||
projectName
|
||||
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
||||
return apiRequest.post("/api/v2/workspace", { projectName, organizationId });
|
||||
};
|
||||
|
||||
export const useCreateWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||
mutationFn: async ({ organizationId, workspaceName }) =>
|
||||
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||
mutationFn: async ({ organizationId, projectName }) =>
|
||||
createWorkspace({
|
||||
organizationId,
|
||||
workspaceName
|
||||
projectName
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
@@ -279,40 +319,16 @@ export const useGetWorkspaceUsers = (workspaceId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddUserToWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ email, workspaceId }: { email: string; workspaceId: string }) => {
|
||||
const {
|
||||
data: { invitee, latestKey }
|
||||
} = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email });
|
||||
|
||||
return {
|
||||
invitee,
|
||||
latestKey
|
||||
};
|
||||
},
|
||||
onSuccess: (_, dto) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(dto.workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteUserFromWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
membershipId,
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
membershipId: string;
|
||||
}) => {
|
||||
mutationFn: async ({ emails, workspaceId }: { workspaceId: string; emails: string[] }) => {
|
||||
const {
|
||||
data: { deletedMembership }
|
||||
} = await apiRequest.delete(`/api/v1/workspace/${workspaceId}/memberships/${membershipId}`);
|
||||
} = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, {
|
||||
data: { emails }
|
||||
});
|
||||
return deletedMembership;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
export enum ProjectVersion {
|
||||
V1 = 1,
|
||||
V2 = 2
|
||||
}
|
||||
|
||||
export type Workspace = {
|
||||
__v: number;
|
||||
id: string;
|
||||
name: string;
|
||||
orgId: string;
|
||||
version: ProjectVersion;
|
||||
upgradeStatus: string | null;
|
||||
autoCapitalization: boolean;
|
||||
environments: WorkspaceEnv[];
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type WorkspaceEnv = {
|
||||
@@ -23,9 +31,16 @@ export type NameWorkspaceSecretsDTO = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TGetUpgradeProjectStatusDTO = {
|
||||
projectId: string;
|
||||
onSuccess?: (data?: { status: string }) => void;
|
||||
enabled?: boolean;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
// mutation dto
|
||||
export type CreateWorkspaceDTO = {
|
||||
workspaceName: string;
|
||||
projectName: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@@ -34,7 +33,6 @@ import * as yup from "yup";
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -51,6 +49,7 @@ import {
|
||||
SelectItem,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { UpgradeOverlay } from "@app/components/v2/UpgradeOverlay";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -62,16 +61,14 @@ import {
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWs,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useGetOrgTrialUrl,
|
||||
useGetSecretApprovalRequestCount,
|
||||
useGetUserAction,
|
||||
useLogoutUser,
|
||||
useRegisterUserAction,
|
||||
useUploadWsKey
|
||||
useRegisterUserAction
|
||||
} from "@app/hooks/api";
|
||||
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||
import { CreateOrgModal } from "@app/views/Org/components";
|
||||
|
||||
interface LayoutProps {
|
||||
@@ -129,8 +126,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
: true;
|
||||
|
||||
const createWs = useCreateWorkspace();
|
||||
const uploadWsKey = useUploadWsKey();
|
||||
const addWsUser = useAddUserToWs();
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
|
||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
@@ -220,59 +217,44 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg?.id) return;
|
||||
if (!currentOrg) return;
|
||||
if (!user) return;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
workspace: { id: newWorkspaceId }
|
||||
project: { id: newProjectId }
|
||||
}
|
||||
} = await createWs.mutateAsync({
|
||||
organizationId: currentOrg?.id,
|
||||
workspaceName: name
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadWsKey.mutateAsync({
|
||||
encryptedKey: ciphertext,
|
||||
nonce,
|
||||
userId: user?.id,
|
||||
workspaceId: newWorkspaceId
|
||||
organizationId: currentOrg.id,
|
||||
projectName: name
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
// not using hooks because need at this point only
|
||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||
const decryptKey = await fetchUserWsKey(newWorkspaceId);
|
||||
await addWsUser.mutateAsync({
|
||||
workspaceId: newWorkspaceId,
|
||||
decryptKey,
|
||||
userPrivateKey: PRIVATE_KEY,
|
||||
members: orgUsers
|
||||
.filter(
|
||||
({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email
|
||||
)
|
||||
.map(({ user: orgUser, id: orgMembershipId }) => ({
|
||||
userPublicKey: orgUser.publicKey,
|
||||
orgMembershipId
|
||||
}))
|
||||
|
||||
await addUsersToProject.mutateAsync({
|
||||
emails: orgUsers
|
||||
.map((member) => member.user.email)
|
||||
.filter((email) => email !== user.email),
|
||||
projectId: newProjectId
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||
|
||||
createNotification({ text: "Workspace created", type: "success" });
|
||||
handlePopUpClose("addNewWs");
|
||||
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
@@ -280,6 +262,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div>
|
||||
<UpgradeOverlay />
|
||||
{!router.asPath.includes("personal") && (
|
||||
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
||||
{(router.asPath.includes("project") ||
|
||||
@@ -310,41 +293,43 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
|
||||
{orgs?.map((org) => {
|
||||
{orgs?.map((org) => {
|
||||
return (
|
||||
<DropdownMenuItem key={org.id}>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (currentOrg?.id === org.id) return;
|
||||
|
||||
if (org.authEnforced) {
|
||||
// org has an org-level auth method enabled (e.g. SAML)
|
||||
// -> logout + redirect to SAML SSO
|
||||
<DropdownMenuItem key={org.id}>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (currentOrg?.id === org.id) return;
|
||||
|
||||
await logout.mutateAsync();
|
||||
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
|
||||
window.close();
|
||||
return;
|
||||
if (org.authEnforced) {
|
||||
// org has an org-level auth method enabled (e.g. SAML)
|
||||
// -> logout + redirect to SAML SSO
|
||||
|
||||
await logout.mutateAsync();
|
||||
window.open(
|
||||
`/api/v1/sso/redirect/saml2/organizations/${org.slug}`
|
||||
);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
changeOrg(org?.id);
|
||||
}}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
size="xs"
|
||||
className="flex w-full items-center justify-start p-0 font-normal"
|
||||
leftIcon={
|
||||
currentOrg?.id === org.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
|
||||
)
|
||||
}
|
||||
|
||||
changeOrg(org?.id)
|
||||
}}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
size="xs"
|
||||
className="flex w-full items-center justify-start p-0 font-normal"
|
||||
leftIcon={
|
||||
currentOrg?.id === org.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{org.name}
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{org.name}
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
{/* <DropdownMenuItem key="add-org">
|
||||
<Button
|
||||
@@ -620,7 +605,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
{(window.location.origin.includes("https://app.infisical.com")) && (
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
||||
<Link href={`/org/${currentOrg?.id}/billing`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -23,7 +21,7 @@ import {
|
||||
faNetworkWired,
|
||||
faPlug,
|
||||
faPlus,
|
||||
faUserPlus,
|
||||
faUserPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
@@ -33,7 +31,6 @@ import * as yup from "yup";
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -54,12 +51,11 @@ import {
|
||||
import { withPermission } from "@app/hoc";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWs,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useRegisterUserAction,
|
||||
useUploadWsKey
|
||||
useRegisterUserAction
|
||||
} from "@app/hooks/api";
|
||||
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -475,7 +471,7 @@ const OrganizationPage = withPermission(
|
||||
const currentOrg = String(router.query.id);
|
||||
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === currentOrg) || [];
|
||||
const { createNotification } = useNotificationContext();
|
||||
const addWsUser = useAddUserToWs();
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
@@ -497,61 +493,39 @@ const OrganizationPage = withPermission(
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const createWs = useCreateWorkspace();
|
||||
const { user } = useUser();
|
||||
const uploadWsKey = useUploadWsKey();
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg) return;
|
||||
if (!user) return;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
workspace: { id: newWorkspaceId }
|
||||
project: { id: newProjectId }
|
||||
}
|
||||
} = await createWs.mutateAsync({
|
||||
organizationId: currentOrg,
|
||||
workspaceName: name
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadWsKey.mutateAsync({
|
||||
encryptedKey: ciphertext,
|
||||
nonce,
|
||||
userId: user?.id,
|
||||
workspaceId: newWorkspaceId
|
||||
projectName: name
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
// not using hooks because need at this point only
|
||||
const orgUsers = await fetchOrgUsers(currentOrg);
|
||||
const decryptKey = await fetchUserWsKey(newWorkspaceId);
|
||||
const members = orgUsers
|
||||
.filter(
|
||||
({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email
|
||||
)
|
||||
.map(({ user: orgUser, id: orgMembershipId }) => ({
|
||||
userPublicKey: orgUser.publicKey,
|
||||
orgMembershipId
|
||||
}));
|
||||
if (members.length) {
|
||||
await addWsUser.mutateAsync({
|
||||
workspaceId: newWorkspaceId,
|
||||
decryptKey,
|
||||
userPrivateKey: PRIVATE_KEY,
|
||||
members
|
||||
});
|
||||
}
|
||||
|
||||
await addUsersToProject.mutateAsync({
|
||||
emails: orgUsers
|
||||
.map((member) => member.user.email)
|
||||
.filter((email) => email !== user.email),
|
||||
projectId: newProjectId
|
||||
});
|
||||
}
|
||||
createNotification({ text: "Workspace created", type: "success" });
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||
|
||||
handlePopUpClose("addNewWs");
|
||||
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
||||
createNotification({ text: "Workspace created", type: "success" });
|
||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||
@@ -735,7 +709,7 @@ 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-6 pb-0 text-3xl">
|
||||
<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
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
useUpdateBotActiveStatus
|
||||
} from "@app/hooks/api";
|
||||
import { IntegrationAuth } from "@app/hooks/api/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
|
||||
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
|
||||
@@ -92,7 +93,7 @@ export const IntegrationsPage = withProjectPermission(
|
||||
isIntegrationsAuthorizedEmpty &&
|
||||
isIntegrationsEmpty
|
||||
) {
|
||||
if (bot?.id)
|
||||
if (bot?.id && currentWorkspace?.version === ProjectVersion.V1)
|
||||
updateBotActiveStatusSync({
|
||||
isActive: false,
|
||||
botId: bot.id,
|
||||
@@ -113,7 +114,7 @@ export const IntegrationsPage = withProjectPermission(
|
||||
if (!selectedCloudIntegration) return;
|
||||
|
||||
try {
|
||||
if (bot && !bot.isActive) {
|
||||
if (bot && !bot.isActive && currentWorkspace?.version === ProjectVersion.V1) {
|
||||
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
|
||||
await updateBotActiveStatus({
|
||||
workspaceId,
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export const OrgMembersSection = () => {
|
||||
const handleAddMemberModal = () => {
|
||||
if (currentOrg?.authEnforced) {
|
||||
createNotification({
|
||||
text: "You cannot invite users when org-level auth is configured for your organization",
|
||||
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
|
||||
+8
@@ -231,6 +231,14 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (currentOrg?.authEnforced) {
|
||||
createNotification({
|
||||
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("removeMember", { orgMembershipId, email });
|
||||
}}
|
||||
size="lg"
|
||||
|
||||
+6
@@ -81,6 +81,12 @@ const SIMPLE_PERMISSION_OPTIONS = [
|
||||
subtitle: "Define organization level SSO requirements",
|
||||
icon: faSignIn,
|
||||
formName: "sso"
|
||||
},
|
||||
{
|
||||
title: "SCIM",
|
||||
subtitle: "Define organization level SCIM requirements",
|
||||
icon: faUsers,
|
||||
formName: "scim"
|
||||
}
|
||||
] as const;
|
||||
|
||||
|
||||
+1
@@ -34,6 +34,7 @@ export const formSchema = z.object({
|
||||
"incident-contact": generalPermissionSchema,
|
||||
"secret-scanning": generalPermissionSchema,
|
||||
sso: generalPermissionSchema,
|
||||
scim: generalPermissionSchema,
|
||||
billing: generalPermissionSchema,
|
||||
identity: generalPermissionSchema
|
||||
})
|
||||
|
||||
@@ -44,7 +44,8 @@ import {
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useAddUserToWs,
|
||||
useAddUserToWsE2EE,
|
||||
useAddUserToWsNonE2EE,
|
||||
useDeleteUserFromWorkspace,
|
||||
useGetOrgUsers,
|
||||
useGetProjectRoles,
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
useUploadWsKey
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const addMemberFormSchema = z.object({
|
||||
orgMembershipId: z.string().trim()
|
||||
@@ -95,12 +97,14 @@ export const MemberListTab = () => {
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
||||
|
||||
const { mutateAsync: addUserToWorkspace } = useAddUserToWs();
|
||||
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
|
||||
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
|
||||
const { mutateAsync: uploadWsKey } = useUploadWsKey();
|
||||
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
||||
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
||||
|
||||
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
|
||||
if (!currentWorkspace) return;
|
||||
if (!currentOrg?.id) return;
|
||||
// TODO(akhilmhdh): Move to memory storage
|
||||
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
|
||||
@@ -114,12 +118,26 @@ export const MemberListTab = () => {
|
||||
if (!orgUser) return;
|
||||
|
||||
try {
|
||||
await addUserToWorkspace({
|
||||
workspaceId,
|
||||
userPrivateKey,
|
||||
decryptKey: wsKey,
|
||||
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
||||
});
|
||||
if (currentWorkspace.version === ProjectVersion.V1) {
|
||||
await addUserToWorkspace({
|
||||
workspaceId,
|
||||
userPrivateKey,
|
||||
decryptKey: wsKey,
|
||||
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
||||
});
|
||||
} else if (currentWorkspace.version === ProjectVersion.V2) {
|
||||
await addUserToWorkspaceNonE2EE({
|
||||
projectId: workspaceId,
|
||||
emails: [orgUser.user.email]
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Failed to add user to project, unknown project type",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
createNotification({
|
||||
text: "Successfully added user to the project",
|
||||
type: "success"
|
||||
@@ -136,11 +154,11 @@ export const MemberListTab = () => {
|
||||
};
|
||||
|
||||
const handleRemoveUser = async () => {
|
||||
const membershipId = (popUp?.removeMember?.data as { id: string })?.id;
|
||||
const email = (popUp?.removeMember?.data as { email: string })?.email;
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
try {
|
||||
await removeUserFromWorkspace({ workspaceId, membershipId });
|
||||
await removeUserFromWorkspace({ workspaceId, emails: [email] });
|
||||
createNotification({
|
||||
text: "Successfully removed user from project",
|
||||
type: "success"
|
||||
@@ -357,7 +375,7 @@ export const MemberListTab = () => {
|
||||
className="ml-4"
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeMember", { id: membershipId })
|
||||
handlePopUpOpen("removeMember", { email: u.email })
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
@@ -441,7 +459,7 @@ export const MemberListTab = () => {
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this user from the org?"
|
||||
title="Do you want to remove this user from the project?"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||
onDeleteApproved={handleRemoveUser}
|
||||
/>
|
||||
|
||||
+12
-2
@@ -9,6 +9,7 @@ import {
|
||||
faXmarkCircle
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { Button, ContentLoader, EmptyState, IconButton, Tooltip } from "@app/components/v2";
|
||||
@@ -170,9 +171,18 @@ export const SecretApprovalRequestChanges = ({
|
||||
<IconButton variant="outline_bg" ariaLabel="go-back" onClick={onGoBack}>
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</IconButton>
|
||||
<div className="flex items-center space-x-2 rounded-3xl bg-red-600 px-4 py-2 text-white">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center space-x-2 rounded-3xl px-4 py-2 text-white",
|
||||
secretApprovalRequestDetails.status === "close" ? "bg-red-600" : "bg-green-600"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCodeBranch} size="sm" />
|
||||
<span>{secretApprovalRequestDetails.status}</span>
|
||||
<span className="capitalize">
|
||||
{secretApprovalRequestDetails.status === "close"
|
||||
? "closed"
|
||||
: secretApprovalRequestDetails.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="mb-1 text-lg">
|
||||
|
||||
+38
-19
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faClock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClock, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -9,20 +9,28 @@ import { z } from "zod";
|
||||
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
|
||||
|
||||
const ReminderFormSchema = z.object({
|
||||
note: z.string().optional(),
|
||||
note: z.string().optional().nullable(),
|
||||
days: z
|
||||
.number()
|
||||
.min(1, { message: "Must be at least 1 day" })
|
||||
.max(365, { message: "Must be less than 365 days" })
|
||||
.nullable()
|
||||
});
|
||||
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
||||
|
||||
interface ReminderFormProps {
|
||||
isOpen: boolean;
|
||||
repeatDays?: number | null;
|
||||
note?: string | null;
|
||||
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
|
||||
}
|
||||
|
||||
export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps) => {
|
||||
export const CreateReminderForm = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
repeatDays,
|
||||
note
|
||||
}: ReminderFormProps) => {
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
@@ -31,32 +39,31 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TReminderFormSchema>({
|
||||
defaultValues: {
|
||||
days: repeatDays || undefined,
|
||||
note: note || ""
|
||||
},
|
||||
resolver: zodResolver(ReminderFormSchema)
|
||||
});
|
||||
|
||||
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
||||
console.log(data);
|
||||
onOpenChange(false, data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
reset();
|
||||
reset({
|
||||
days: repeatDays || undefined,
|
||||
note: note || ""
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Create secret reminder"
|
||||
// ? QUESTION: Should this specifically say its for secret rotation?
|
||||
// ? Or should we be call it something more generic?
|
||||
subTitle={
|
||||
<div>
|
||||
Set up a reminder for when this secret should be rotated. Everyone with access to this
|
||||
project will be notified when the reminder is triggered.
|
||||
</div>
|
||||
}
|
||||
title={`${repeatDays ? "Update" : "Create"} reminder`}
|
||||
subTitle="Set up a reminder for when this secret should be rotated. Everyone with access to this project will be notified when the reminder is triggered."
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-2">
|
||||
@@ -68,7 +75,7 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
||||
<>
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
label="How many days between"
|
||||
label="Reminder Interval (in days)"
|
||||
isError={Boolean(fieldState.error)}
|
||||
errorText={fieldState.error?.message || ""}
|
||||
>
|
||||
@@ -76,6 +83,7 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
||||
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
|
||||
type="number"
|
||||
placeholder="31"
|
||||
value={field.value || undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
<div
|
||||
@@ -84,7 +92,8 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
||||
field.value ? "opacity-60" : "opacity-0"
|
||||
)}
|
||||
>
|
||||
Every {field.value > 1 ? `${field.value} days` : "day"}
|
||||
A reminder will be sent every{" "}
|
||||
{field.value && field.value > 1 ? `${field.value} days` : "day"}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -102,17 +111,27 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="mt-7 flex items-center">
|
||||
<div className="mt-7 flex items-center space-x-4">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
className=""
|
||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||
type="submit"
|
||||
>
|
||||
Create reminder
|
||||
{repeatDays ? "Update" : "Create"} reminder
|
||||
</Button>
|
||||
{repeatDays && (
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => onOpenChange(false, { days: null, note: null })}
|
||||
colorSchema="danger"
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete reminder
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => onOpenChange(false)}
|
||||
|
||||
+297
-239
@@ -5,6 +5,7 @@ import {
|
||||
faCheckCircle,
|
||||
faCircle,
|
||||
faCircleDot,
|
||||
faClock,
|
||||
faPlus,
|
||||
faTag
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -33,9 +34,11 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetSecretVersion } from "@app/hooks/api";
|
||||
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
|
||||
|
||||
import { CreateReminderForm } from "./CreateReminderForm";
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
@@ -147,262 +150,317 @@ export const SecretDetailSidebar = ({
|
||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||
};
|
||||
|
||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||
|
||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||
const secretReminderNote = watch("reminderNote");
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
onOpenChange={(state) => {
|
||||
if (isOpen && isDirty) {
|
||||
if (
|
||||
// eslint-disable-next-line no-alert
|
||||
window.confirm("You have edited the secret. Are you sure you want to reset the change?")
|
||||
) {
|
||||
onToggle(false);
|
||||
reset();
|
||||
} else return;
|
||||
}
|
||||
onToggle(state);
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<DrawerContent title="Secret">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
|
||||
<div className="flex h-full flex-col">
|
||||
<FormControl label="Key">
|
||||
<Input isDisabled {...register("key")} />
|
||||
</FormControl>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
name="value"
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value">
|
||||
<SecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
key="secret-value"
|
||||
isDisabled={isOverridden || !isAllowed}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
||||
<>
|
||||
<CreateReminderForm
|
||||
repeatDays={secretReminderRepeatDays}
|
||||
note={secretReminderNote}
|
||||
isOpen={createReminderFormOpen}
|
||||
onOpenChange={(_, data) => {
|
||||
setCreateReminderFormOpen.toggle();
|
||||
|
||||
if (data) {
|
||||
setValue("reminderRepeatDays", data.days, { shouldDirty: true });
|
||||
setValue("reminderNote", data.note, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Drawer
|
||||
onOpenChange={(state) => {
|
||||
if (isOpen && isDirty) {
|
||||
if (
|
||||
// eslint-disable-next-line no-alert
|
||||
window.confirm(
|
||||
"You have edited the secret. Are you sure you want to reset the change?"
|
||||
)
|
||||
) {
|
||||
onToggle(false);
|
||||
reset();
|
||||
} else return;
|
||||
}
|
||||
onToggle(state);
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<DrawerContent title="Secret">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
|
||||
<div className="flex h-full flex-col">
|
||||
<FormControl label="Key">
|
||||
<Input isDisabled {...register("key")} />
|
||||
</FormControl>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
isDisabled={!isAllowed}
|
||||
id="personal-override"
|
||||
onCheckedChange={handleOverrideClick}
|
||||
isChecked={isOverridden}
|
||||
>
|
||||
Override with a personal value
|
||||
</Switch>
|
||||
<Controller
|
||||
name="value"
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value">
|
||||
<SecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
key="secret-value"
|
||||
isDisabled={isOverridden || !isAllowed}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{isOverridden && (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value Override">
|
||||
<SecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormControl label="Tags" className="">
|
||||
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
||||
{fields.map(({ tagColor, id: formId, name, id }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={formId}
|
||||
onClose={() => {
|
||||
if (cannotEditSecret) {
|
||||
createNotification({ type: "error", text: "Access denied" });
|
||||
return;
|
||||
}
|
||||
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
||||
if (tag) handleTagSelect(tag);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
</Tag>
|
||||
))}
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="add"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="rounded-md"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end" className="z-[100]">
|
||||
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, name, color } = tag;
|
||||
|
||||
const isSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={tagId}
|
||||
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Tags}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||
onClick={onCreateTag}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormControl label="Comments & Notes">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
{...register("comment")}
|
||||
readOnly={isReadOnly}
|
||||
rows={5}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="my-2 mb-6 border-b border-mineshaft-600 pb-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="skipMultilineEncoding"
|
||||
render={({ field: { value, onChange, onBlur } }) => (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="skipmultiencoding-option"
|
||||
onCheckedChange={(isChecked) => onChange(!isChecked)}
|
||||
isChecked={!value}
|
||||
onBlur={onBlur}
|
||||
isDisabled={!isAllowed}
|
||||
className="items-center"
|
||||
>
|
||||
Enable multi line encoding
|
||||
<Tooltip
|
||||
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
|
||||
className="z-[100]"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
|
||||
<div className="mb-2">Version History</div>
|
||||
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(({ createdAt, value, id }, i) => (
|
||||
<div key={id} className="flex flex-col space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||
<div className="break-all font-mono">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isFullWidth
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
||||
isLoading={isSubmitting}
|
||||
<Switch
|
||||
isDisabled={!isAllowed}
|
||||
id="personal-override"
|
||||
onCheckedChange={handleOverrideClick}
|
||||
isChecked={isOverridden}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
||||
Delete
|
||||
</Button>
|
||||
Override with a personal value
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{isOverridden && (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value Override">
|
||||
<SecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormControl label="Tags" className="">
|
||||
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
||||
{fields.map(({ tagColor, id: formId, name, id }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={formId}
|
||||
onClose={() => {
|
||||
if (cannotEditSecret) {
|
||||
createNotification({ type: "error", text: "Access denied" });
|
||||
return;
|
||||
}
|
||||
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
||||
if (tag) handleTagSelect(tag);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
</Tag>
|
||||
))}
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="add"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="rounded-md"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end" className="z-[100]">
|
||||
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, name, color } = tag;
|
||||
|
||||
const isSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={tagId}
|
||||
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Tags}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||
onClick={onCreateTag}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormControl label="Reminder">
|
||||
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
||||
<div className="mt-2 ml-1 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
||||
<span className="text-sm text-bunker-300">
|
||||
Reminder every {secretReminderRepeatDays}{" "}
|
||||
{secretReminderRepeatDays > 1 ? "days" : "day"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className="px-2 py-1"
|
||||
variant="outline_bg"
|
||||
onClick={() => setCreateReminderFormOpen.on()}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 ml-1 flex items-center space-x-2">
|
||||
<Button
|
||||
className="px-2 py-1"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||
onClick={() => setCreateReminderFormOpen.on()}
|
||||
>
|
||||
Create Reminder
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl label="Comments & Notes">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
{...register("comment")}
|
||||
readOnly={isReadOnly}
|
||||
rows={5}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="my-2 mb-6 border-b border-mineshaft-600 pb-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="skipMultilineEncoding"
|
||||
render={({ field: { value, onChange, onBlur } }) => (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="skipmultiencoding-option"
|
||||
onCheckedChange={(isChecked) => onChange(!isChecked)}
|
||||
isChecked={!value}
|
||||
onBlur={onBlur}
|
||||
isDisabled={!isAllowed}
|
||||
className="items-center"
|
||||
>
|
||||
Enable multi line encoding
|
||||
<Tooltip
|
||||
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
|
||||
className="z-[100]"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
|
||||
<div className="mb-2">Version History</div>
|
||||
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(({ createdAt, value, id }, i) => (
|
||||
<div key={id} className="flex flex-col space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||
<div className="break-all font-mono">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isFullWidth
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</form>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -111,9 +111,11 @@ export const SecretItem = memo(
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||
const secretReminderNote = watch("reminderNote");
|
||||
|
||||
const overrideAction = watch("overrideAction");
|
||||
const hasComment = Boolean(watch("comment"));
|
||||
const hasReminder = Boolean(watch("reminderRepeatDays"));
|
||||
|
||||
const selectedTags = watch("tags", []);
|
||||
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
||||
@@ -191,6 +193,8 @@ export const SecretItem = memo(
|
||||
return (
|
||||
<>
|
||||
<CreateReminderForm
|
||||
repeatDays={secretReminderRepeatDays}
|
||||
note={secretReminderNote}
|
||||
isOpen={createReminderFormOpen}
|
||||
onOpenChange={(_, data) => {
|
||||
setCreateReminderFormOpen.toggle();
|
||||
@@ -380,22 +384,24 @@ export const SecretItem = memo(
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
||||
hasReminder && "w-5 text-primary"
|
||||
Boolean(secretReminderRepeatDays) && "w-5 text-primary"
|
||||
)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-reminder"
|
||||
>
|
||||
<Tooltip content="Reminder">
|
||||
<Tooltip
|
||||
content={
|
||||
secretReminderRepeatDays && secretReminderRepeatDays > 0
|
||||
? `Every ${secretReminderRepeatDays} day${
|
||||
Number(secretReminderRepeatDays) > 1 ? "s" : ""
|
||||
}
|
||||
`
|
||||
: "Reminder"
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
onClick={() => {
|
||||
if (!hasReminder) {
|
||||
setCreateReminderFormOpen.on();
|
||||
} else {
|
||||
setValue("reminderRepeatDays", null, { shouldDirty: true });
|
||||
setValue("reminderNote", null, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
onClick={() => setCreateReminderFormOpen.on()}
|
||||
icon={faClock}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
// import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useCreateFolder,
|
||||
@@ -40,9 +41,9 @@ import {
|
||||
useUpdateSecretV3
|
||||
} from "@app/hooks/api";
|
||||
|
||||
// import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
|
||||
// import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
|
||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
||||
|
||||
@@ -315,6 +316,8 @@ export const SecretOverviewPage = () => {
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||
<div className="w-80">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection";
|
||||
import { OrgScimSection } from "./OrgSCIMSection";
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
|
||||
export const OrgAuthTab = withPermission(
|
||||
@@ -10,6 +11,7 @@ export const OrgAuthTab = withPermission(
|
||||
<div>
|
||||
<OrgGeneralAuthSection />
|
||||
<OrgSSOSection />
|
||||
<OrgScimSection />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
+22
-4
@@ -1,16 +1,25 @@
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Switch } from "@app/components/v2";
|
||||
import {
|
||||
Switch,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useLogoutUser,useUpdateOrg } from "@app/hooks/api";
|
||||
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
export const OrgGeneralAuthSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync } = useUpdateOrg();
|
||||
|
||||
@@ -19,6 +28,10 @@ export const OrgGeneralAuthSection = () => {
|
||||
const handleEnforceOrgAuthToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (!subscription?.samlSSO) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
@@ -39,7 +52,7 @@ export const OrgGeneralAuthSection = () => {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${value ? "enforce" : "un-enforce"} org-level auth`,
|
||||
text: (err as { response: { data: { message: string; }}}).response.data.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
@@ -60,6 +73,11 @@ export const OrgGeneralAuthSection = () => {
|
||||
</Switch>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can enforce SAML SSO if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Switch,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription} from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { ScimTokenModal } from "./ScimTokenModal";
|
||||
|
||||
export const OrgScimSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"scimToken",
|
||||
"deleteScimToken",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync } = useUpdateOrg();
|
||||
|
||||
const addScimTokenBtnClick = () => {
|
||||
if (subscription?.scim) {
|
||||
handlePopUpOpen("scimToken");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnableSCIMToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (!subscription?.scim) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
scimEnabled: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enabled" : "disabled"} SCIM provisioning`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: (err as { response: { data: { message: string; }}}).response.data.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-8 flex items-center">
|
||||
<h2 className="flex-1 text-xl font-semibold text-white">SCIM Configuration</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Scim}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={addScimTokenBtnClick}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Manage SCIM Tokens
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enable-scim"
|
||||
onCheckedChange={(value) => {
|
||||
if (subscription?.scim) {
|
||||
handleEnableSCIMToggle(value)
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
isChecked={currentOrg?.scimEnabled ?? false}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Enable SCIM Provisioning
|
||||
</Switch>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<ScimTokenModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use SCIM Provisioning if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { format } from "date-fns";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
useCreateScimToken,
|
||||
useDeleteScimToken,
|
||||
useGetScimTokens} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup.object({
|
||||
description: yup.string(),
|
||||
ttlDays: yup.string().required("TTL is required")
|
||||
});
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["scimToken", "deleteScimToken"]>;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deleteScimToken"]>,
|
||||
data?: {
|
||||
scimTokenId: string;
|
||||
}
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["scimToken", "deleteScimToken"]
|
||||
>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const ScimTokenModal = ({
|
||||
popUp,
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const [token, setToken] = useState("");
|
||||
|
||||
const [isScimUrlCopied, setIsScimUrlCopied] = useToggle(false);
|
||||
const [isScimTokenCopied, setIsScimTokenCopied] = useToggle(false);
|
||||
|
||||
const { data, isLoading } = useGetScimTokens(currentOrg?.id ?? "");
|
||||
const { mutateAsync: createScimTokenMutateAsync } = useCreateScimToken();
|
||||
const { mutateAsync: deleteScimTokenMutateAsync } = useDeleteScimToken();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
description: "",
|
||||
ttlDays: "365"
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isScimUrlCopied) {
|
||||
timer = setTimeout(() => setIsScimUrlCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isScimTokenCopied) {
|
||||
timer = setTimeout(() => setIsScimTokenCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isScimTokenCopied, isScimUrlCopied]);
|
||||
|
||||
const onFormSubmit = async ({ description, ttlDays }: FormData) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
const { scimToken } = await createScimTokenMutateAsync({
|
||||
organizationId: currentOrg.id,
|
||||
description,
|
||||
ttlDays: Number(ttlDays)
|
||||
});
|
||||
|
||||
setToken(scimToken);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created SCIM token",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create SCIM token",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteScimTokenSubmit = async (scimTokenId: string) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
await deleteScimTokenMutateAsync({
|
||||
organizationId: currentOrg.id,
|
||||
scimTokenId
|
||||
});
|
||||
|
||||
handlePopUpToggle("deleteScimToken", false);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted SCIM token",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete SCIM token",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hasToken = Boolean(token);
|
||||
const scimUrl = `${window.origin}/api/v1/scim`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.scimToken?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("scimToken", isOpen);
|
||||
reset();
|
||||
setToken("");
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Manage SCIM credentials">
|
||||
<h2 className="mb-4">SCIM URL</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{scimUrl}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(scimUrl);
|
||||
setIsScimUrlCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isScimUrlCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{t("common.click-to-copy")}
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<h2 className="mb-4">New SCIM Token</h2>
|
||||
{hasToken ? (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p>We will only show this token once</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
reset();
|
||||
setToken("");
|
||||
}}
|
||||
>
|
||||
Got it
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{token}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(token);
|
||||
setIsScimTokenCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isScimTokenCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{t("common.click-to-copy")}
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="description"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Description (optional)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Description" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="365"
|
||||
name="ttlDays"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="TTL (days)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<div className="flex">
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
<Button
|
||||
className="ml-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
<h2 className="mb-4">SCIM Tokens</h2>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Description</Th>
|
||||
<Th>Expires At</Th>
|
||||
<Th>Created At</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="org-scim-tokens" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(
|
||||
({
|
||||
id,
|
||||
description,
|
||||
ttlDays,
|
||||
createdAt
|
||||
}) => {
|
||||
|
||||
let expiresAt;
|
||||
if (ttlDays > 0) {
|
||||
expiresAt = new Date(new Date(createdAt).getTime() + ttlDays * 86400 * 1000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
|
||||
<Td>{description === "" ? "-" : description}</Td>
|
||||
<Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd HH:mm:ss") : "-"}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd HH:mm:ss")}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteScimToken", {
|
||||
scimTokenId: id
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={4}>
|
||||
<EmptyState
|
||||
title="No SCIM tokens have been created yet"
|
||||
icon={faKey}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteScimToken.isOpen}
|
||||
title="Are you sure want to delete the SCIM token?"
|
||||
onChange={(isOpen) => handlePopUpToggle("scimToken", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => {
|
||||
|
||||
const deleteScimTokenData = popUp?.deleteScimToken?.data as {
|
||||
scimTokenId: string;
|
||||
};
|
||||
|
||||
return onDeleteScimTokenSubmit(deleteScimTokenData.scimTokenId);
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -170,7 +170,11 @@ export const AddAPIKeyModal = ({
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("addAPIKey", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+5
-1
@@ -6,6 +6,7 @@ import {
|
||||
import { Alert, AlertDescription, Checkbox } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const E2EESection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -20,6 +21,7 @@ export const E2EESection = () => {
|
||||
* 3. Encrypt project key with bot's public key
|
||||
* 4. Send encrypted project key to backend and set bot status to active
|
||||
*/
|
||||
|
||||
const toggleBotActivate = async () => {
|
||||
let botKey;
|
||||
try {
|
||||
@@ -75,7 +77,9 @@ export const E2EESection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
return bot ? (
|
||||
if (!currentWorkspace) return null;
|
||||
|
||||
return bot && currentWorkspace.version === ProjectVersion.V1 ? (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
|
||||
<p className="mb-8 text-gray-400">
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ export const WebhooksTab = withProjectPermission(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
|
||||
<ProjectPermissionCan
|
||||
|
||||
@@ -84,6 +84,10 @@ export const SignUpPage = () => {
|
||||
tag: userPass.encryptedPrivateKeyTag,
|
||||
privateKey
|
||||
});
|
||||
// TODO(akhilmhdh): This is such a confusing pattern and too unreliable
|
||||
// Will be refactored in next iteration to make it url based rather than local storage ones
|
||||
// Part of migration to nextjs 14
|
||||
localStorage.setItem("orgData.id", res.organization.id);
|
||||
setStep(SignupSteps.BackupKey);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
@@ -130,8 +134,8 @@ export const SignUpPage = () => {
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-2 text-center">
|
||||
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||
<div className="text-4xl pt-4">Welcome to Infisical</div>
|
||||
<div className="text-bunker-300 pb-4">Create your first Super Admin Account</div>
|
||||
<div className="pt-4 text-4xl">Welcome to Infisical</div>
|
||||
<div className="pb-4 text-bunker-300">Create your first Super Admin Account</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="mt-8">
|
||||
@@ -199,7 +203,14 @@ export const SignUpPage = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" colorSchema="primary" variant="outline_bg" isFullWidth className="mt-4" isLoading={isSubmitting}>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isFullWidth
|
||||
className="mt-4"
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user