Merge remote-tracking branch 'origin' into stv3-update

This commit is contained in:
Tuan Dang
2023-11-03 22:38:29 +02:00
19 changed files with 263 additions and 233 deletions

View File

@@ -21,7 +21,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
allowedLabel,
...props
}) => {
const permission = useOrgPermission();
const { permission } = useOrgPermission();
return (
<Can {...props} passThrough={passThrough} ability={props?.ability || permission}>

View File

@@ -1,6 +1,7 @@
import { createContext, ReactNode, useContext } from "react";
import { useGetUserOrgPermissions } from "@app/hooks/api";
import { OrgUser } from "@app/hooks/api/types";
import { useOrganization } from "../OrganizationContext";
import { TOrgPermission } from "./types";
@@ -9,7 +10,10 @@ type Props = {
children: ReactNode;
};
const OrgPermissionContext = createContext<null | TOrgPermission>(null);
const OrgPermissionContext = createContext<null | {
permission: TOrgPermission;
membership: OrgUser | null;
}>(null);
export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
const { currentOrg } = useOrganization();

View File

@@ -21,7 +21,7 @@ export const withPermission = <T extends {}, J extends TOrgPermission>(
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
) => {
const HOC = (hocProps: T) => {
const permission = useOrgPermission();
const { permission } = useOrgPermission();
// akhilmhdh: Set as any due to casl/react ts type bug
// REASON: casl due to its type checking can't seem to union even if union intersection is applied

View File

@@ -8,6 +8,7 @@ import { apiRequest } from "@app/config/request";
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types";
import { OrgUser } from "../users/types";
import {
TGetRolesDTO,
TGetUserOrgPermissionsDTO,
@@ -63,13 +64,16 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) =>
});
const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
if (orgId === "") return [];
if (orgId === "") return { permissions: [], membership: null };
const { data } = await apiRequest.get<{
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
}>(`/api/v1/roles/organization/${orgId}/permissions`, {});
return data.data.permissions;
data: {
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
membership: OrgUser;
};
}>(`/api/v1/roles/organization/${orgId}/permissions`);
return data.data;
};
export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =>
@@ -78,9 +82,9 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
queryFn: () => getUserOrgPermissions({ orgId }),
// enabled: Boolean(orgId),
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data);
const rule = unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data.permissions);
const ability = createMongoAbility<OrgPermissionSet>(rule, { conditionsMatcher });
return ability;
return { permission: ability, membership: data.membership };
}
});

View File

@@ -34,7 +34,8 @@ export const workspaceKeys = {
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const,
getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const,
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const,
getWorkspaceServiceTokenDataV3: (workspaceId: string) => [{ workspaceId }, "workspace-service-token-data-v3"] as const
getWorkspaceServiceTokenDataV3: (workspaceId: string) =>
[{ workspaceId }, "workspace-service-token-data-v3"] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
@@ -53,7 +54,7 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
return data;
};
const fetchWorkspaceSecrets = async (workspaceId: string) => {
export const fetchWorkspaceSecrets = async (workspaceId: string) => {
const {
data: { secrets }
} = await apiRequest.get<{ secrets: EncryptedSecret[] }>(
@@ -253,9 +254,18 @@ export const useReorderWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, ReorderEnvironmentsDTO>({
mutationFn: ({ workspaceID, environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName}) => {
mutationFn: ({
workspaceID,
environmentSlug,
environmentName,
otherEnvironmentSlug,
otherEnvironmentName
}) => {
return apiRequest.patch(`/api/v2/workspace/${workspaceID}/environments`, {
environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName
environmentSlug,
environmentName,
otherEnvironmentSlug,
otherEnvironmentName
});
},
onSuccess: () => {
@@ -378,4 +388,4 @@ export const useGetWorkspaceServiceTokenDataV3 = (workspaceId: string) => {
},
enabled: true
});
};
};

View File

@@ -54,7 +54,7 @@ export const IntegrationsSection = ({
</div>
)}
{!isBotActive && (
{!isBotActive && Boolean(integrations.length) && (
<div className="px-6 py-4">
<Alert hideTitle variant="warning">
<AlertDescription>
@@ -119,7 +119,7 @@ export const IntegrationsSection = ({
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
{(integration.integration === "qovery") && (
{integration.integration === "qovery" && (
<div className="flex flex-row">
<div className="ml-2 flex flex-col">
<FormLabel label="Org" />

View File

@@ -41,6 +41,7 @@ import {
} from "@app/hooks/api";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
@@ -259,6 +260,7 @@ export const SecretOverviewPage = () => {
return (
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
<div className="relative right-5 ml-4">
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
</div>

View File

@@ -0,0 +1,98 @@
import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Button, Spinner } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetWorkspaceIndexStatus, useNameWorkspaceSecrets } from "@app/hooks/api";
import { UserWsKeyPair } from "@app/hooks/api/types";
import { fetchWorkspaceSecrets } from "@app/hooks/api/workspace/queries";
// TODO: add check so that this only shows up if user is
// an admin in the workspace
type Props = {
decryptFileKey: UserWsKeyPair;
};
export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
currentWorkspace?._id ?? ""
);
const [isIndexing, setIsIndexing] = useToggle();
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const onEnableBlindIndices = async () => {
if (!currentWorkspace?._id) return;
setIsIndexing.on();
try {
const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace._id);
const key = decryptAssymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
return {
secretName,
_id: encryptedSecret._id
};
});
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace._id,
secretsToUpdate
});
} catch (err) {
console.log(err);
} finally {
setIsIndexing.off();
}
};
return !isBlindIndexedLoading && !isBlindIndexed ? (
<div className="p-4 mt-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
{isIndexing && (
<div className="w-screen absolute top-0 left-0 h-screen z-50 bg-bunker-500 bg-opacity-80 flex items-center justify-center">
<Spinner size="lg" className="text-primary" />
<div className="flex flex-col space-y-1 ml-4">
<div className="text-3xl font-medium">Please wait</div>
<span className="inline-block">Re-indexing your secrets...</span>
</div>
</div>
)}
<p className="mb-2 text-lg font-semibold">Enable Blind Indices</p>
<p className="text-gray-400 mb-4 leading-7">
Your project was created before the introduction of blind indexing.
To continue accessing secrets by name through the SDK, public API and web dashboard, please enable blind
indexing. <b>This is a one time process.</b>
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Button
onClick={onEnableBlindIndices}
isDisabled={!isAllowed}
color="mineshaft"
type="submit"
isLoading={isIndexing}
>
Enable Blind Indexing
</Button>
)}
</ProjectPermissionCan>
</div>
) : (
<div />
);
};

View File

@@ -1,12 +1,11 @@
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Modal, ModalContent, Step, Stepper } from "@app/components/v2";
import { useCreateSecretRotation } from "@app/hooks/api";
import { TSecretRotationProvider } from "@app/hooks/api/types";
import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider";
import { RotationInputForm } from "./steps/RotationInputForm";
import {
RotationOutputForm,

View File

@@ -1,81 +1,70 @@
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { useOrganization, useUser } from "@app/context";
import {
useDeleteOrgById,
useGetOrgUsers
} from "@app/hooks/api";
import { Button, DeleteActionModal } from "@app/components/v2";
import { useOrganization, useOrgPermission } from "@app/context";
import { useDeleteOrgById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
export const OrgDeleteSection = () => {
const router = useRouter();
const { currentOrg } = useOrganization();
const { user } = useUser();
const { createNotification } = useNotificationContext();
const { data: members } = useGetOrgUsers(currentOrg?._id ?? "");
const membershipOrg = members?.find((member) => member.user._id === user._id);
const router = useRouter();
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { membership } = useOrgPermission();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteOrg"
] as const);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteOrg"
] as const);
const { mutateAsync, isLoading } = useDeleteOrgById();
const handleDeleteOrgSubmit = async () => {
try {
if (!currentOrg?._id) return;
await mutateAsync({
organizationId: currentOrg?._id
});
createNotification({
text: "Successfully deleted organization",
type: "success"
});
const { mutateAsync, isLoading } = useDeleteOrgById();
await navigateUserToOrg(router);
handlePopUpClose("deleteOrg");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete organization",
type: "error"
});
}
const handleDeleteOrgSubmit = async () => {
try {
if (!currentOrg?._id) return;
await mutateAsync({
organizationId: currentOrg?._id
});
createNotification({
text: "Successfully deleted organization",
type: "success"
});
await navigateUserToOrg(router);
handlePopUpClose("deleteOrg");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete organization",
type: "error"
});
}
};
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Danger Zone
</p>
<Button
isLoading={isLoading}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteOrg")}
isDisabled={(membershipOrg && membershipOrg.role !== "admin")}
>
{`Delete ${currentOrg?.name}`}
</Button>
<DeleteActionModal
isOpen={popUp.deleteOrg.isOpen}
title="Are you sure want to delete this organization?"
subTitle={`Permanently remove ${currentOrg?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteOrg", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteOrgSubmit}
/>
</div>
);
}
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Danger Zone</p>
<Button
isLoading={isLoading}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteOrg")}
isDisabled={Boolean(membership && membership.role !== "admin")}
>
{`Delete ${currentOrg?.name}`}
</Button>
<DeleteActionModal
isOpen={popUp.deleteOrg.isOpen}
title="Are you sure want to delete this organization?"
subTitle={`Permanently remove ${currentOrg?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteOrg", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteOrgSubmit}
/>
</div>
);
};

View File

@@ -1,24 +1,17 @@
import { useOrganization, useUser } from "@app/context";
import { useGetOrgUsers } from "@app/hooks/api";
import { useOrgPermission } from "@app/context";
import { OrgDeleteSection } from "../OrgDeleteSection";
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
import { OrgNameChangeSection } from "../OrgNameChangeSection";
export const OrgGeneralTab = () => {
const { currentOrg } = useOrganization();
const { user } = useUser();
const { data: members } = useGetOrgUsers(currentOrg?._id ?? "");
const membershipOrg = members?.find((member) => member.user._id === user?._id);
const { membership } = useOrgPermission();
return (
<div>
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{(membershipOrg && membershipOrg.role === "admin") && (
<OrgDeleteSection />
)}
{membership && membership.role === "admin" && <OrgDeleteSection />}
</div>
);
};

View File

@@ -15,7 +15,7 @@ export const OrgIncidentContactsSection = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact"
] as const);
const permission = useOrgPermission();
const { permission } = useOrgPermission();
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">

View File

@@ -2,20 +2,18 @@ import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { E2EESection } from "../E2EESection";
import { EnvironmentSection } from "../EnvironmentSection";
import { ProjectIndexSecretsSection } from "../ProjectIndexSecretsSection";
import { ProjectNameChangeSection } from "../ProjectNameChangeSection";
import { SecretTagsSection } from "../SecretTagsSection";
export const ProjectGeneralTab = () => {
return (
<div>
<ProjectNameChangeSection />
<EnvironmentSection />
<SecretTagsSection />
<AutoCapitalizationSection />
<ProjectIndexSecretsSection />
<E2EESection />
<DeleteProjectSection />
</div>
);
}
return (
<div>
<ProjectNameChangeSection />
<EnvironmentSection />
<SecretTagsSection />
<AutoCapitalizationSection />
<E2EESection />
<DeleteProjectSection />
</div>
);
};

View File

@@ -1,84 +0,0 @@
import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Button } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import {
useGetUserWsKey,
useGetWorkspaceIndexStatus,
useGetWorkspaceSecrets,
useNameWorkspaceSecrets
} from "@app/hooks/api";
// TODO: add check so that this only shows up if user is
// an admin in the workspace
export const ProjectIndexSecretsSection = () => {
const { currentWorkspace } = useWorkspace();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
currentWorkspace?._id ?? ""
);
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const onEnableBlindIndices = async () => {
if (!currentWorkspace?._id) return;
if (!encryptedSecrets) return;
if (!latestFileKey) return;
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
return {
secretName,
_id: encryptedSecret._id
};
});
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace._id,
secretsToUpdate
});
};
return !isBlindIndexedLoading && !isBlindIndexed ? (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
<p className="text-gray-400 mb-8">
Your project, created before the introduction of blind indexing, contains unindexed secrets.
To access individual secrets by name through the SDK and public API, please enable blind
indexing.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Button
onClick={onEnableBlindIndices}
isDisabled={!isAllowed}
color="mineshaft"
size="sm"
type="submit"
>
Enable Blind Indexing
</Button>
)}
</ProjectPermissionCan>
</div>
) : (
<div />
);
};

View File

@@ -2,7 +2,6 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
export { DeleteProjectSection } from "./DeleteProjectSection";
export { E2EESection } from "./E2EESection";
export { EnvironmentSection } from "./EnvironmentSection";
export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection";
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
export { SecretTagsSection } from "./SecretTagsSection";
export { ServiceTokenSection } from "./ServiceTokenSection";