Finish preliminary delete user, organization, refactor delete project, create org page

This commit is contained in:
Tuan Dang
2023-10-09 07:47:20 +01:00
parent ee80f4a89b
commit 9fcecc9c92
53 changed files with 1279 additions and 299 deletions
@@ -2,8 +2,6 @@
import jsrp from "jsrp";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import KeyService from "@app/services/KeyService";
import Telemetry from "./telemetry/Telemetry";
@@ -122,17 +120,7 @@ const attemptLogin = async (
iv,
tag,
privateKey
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
if (orgUserProjects.length > 0) {
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
}
});
if (email) {
telemetry.identify(email, email);
@@ -2,9 +2,6 @@
import jsrp from "jsrp";
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
// import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken";
import KeyService from "@app/services/KeyService";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
@@ -19,7 +16,6 @@ interface IsMfaLoginSuccessful {
privateKey: string;
JTWToken: string;
}
}
/**
@@ -93,16 +89,6 @@ const attemptLoginMfa = async ({
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
resolve({
success: true,
loginResponse:{
@@ -2,8 +2,6 @@
import jsrp from "jsrp";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import KeyService from "@app/services/KeyService";
import Telemetry from "./telemetry/Telemetry";
@@ -119,20 +117,6 @@ const attemptLogin = async (
tag,
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
if (orgUserProjects.length > 0) {
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
}
if (email) {
telemetry.identify(email, email);
@@ -2,8 +2,6 @@
import jsrp from "jsrp";
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import KeyService from "@app/services/KeyService";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
@@ -83,16 +81,6 @@ const attemptLoginMfa = async ({
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
resolve(true);
} catch (err) {
reject(err);
@@ -2,6 +2,8 @@ export {
useAddOrgPmtMethod,
useAddOrgTaxId,
useCreateCustomerPortalSession,
useCreateOrg,
useDeleteOrgById,
useDeleteOrgPmtMethod,
useDeleteOrgTaxId,
useGetOrganizations,
@@ -41,6 +41,25 @@ export const useGetOrganizations = () => {
});
}
export const useCreateOrg = () => {
return useMutation({
mutationFn: async ({
name
}: {
name: string;
}) => {
const { data: { organization } } = await apiRequest.post(
"/api/v2/organizations",
{
name
}
);
return organization;
}
});
};
export const useRenameOrg = () => {
const queryClient = useQueryClient();
@@ -333,4 +352,33 @@ export const useGetOrgLicenses = (organizationId: string) => {
},
enabled: true
});
}
export const useDeleteOrgById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
}: {
organizationId: string;
}) => {
const { data: { organization } } = await apiRequest.delete<{ organization: Organization }>(
`/api/v2/organizations/${organizationId}`
);
return organization;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
queryClient.invalidateQueries(organizationKeys.getOrgPlanBillingInfo(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgPlanTable(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgPlansTable(dto.organizationId, "monthly")); // You might need to invalidate for 'yearly' as well.
queryClient.invalidateQueries(organizationKeys.getOrgPlansTable(dto.organizationId, "yearly"));
queryClient.invalidateQueries(organizationKeys.getOrgBillingDetails(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgInvoices(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgLicenses(dto.organizationId));
}
});
}
+4 -2
View File
@@ -63,10 +63,12 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) =>
});
const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
if (orgId === "") return [];
const { data } = await apiRequest.get<{
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
}>(`/api/v1/roles/organization/${orgId}/permissions`, {});
return data.data.permissions;
};
@@ -74,7 +76,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
useQuery({
queryKey: roleQueryKeys.getUserOrgPermissions({ orgId }),
queryFn: () => getUserOrgPermissions({ orgId }),
enabled: Boolean(orgId),
// enabled: Boolean(orgId),
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data);
const ability = createMongoAbility<OrgPermissionSet>(rule, { conditionsMatcher });
+2 -2
View File
@@ -5,6 +5,7 @@ export {
useCreateAPIKey,
useDeleteAPIKey,
useDeleteOrgMembership,
useDeleteUser,
useGetMyAPIKeys,
useGetMyIp,
useGetMyOrganizationProjects,
@@ -17,5 +18,4 @@ export {
useRevokeMySessions,
useUpdateMfaEnabled,
useUpdateOrgUserRole,
useUpdateUserAuthMethods
} from "./queries";
useUpdateUserAuthMethods} from "./queries";
+28 -5
View File
@@ -42,6 +42,24 @@ export const fetchUserDetails = async () => {
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const { data: { user } } = await apiRequest.delete<{ user: User }>("/api/v2/users/me");
return user;
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.getUser);
queryClient.invalidateQueries(userKeys.userAction);
queryClient.invalidateQueries(userKeys.myIp);
queryClient.invalidateQueries(userKeys.myAPIKeys);
queryClient.invalidateQueries(userKeys.mySessions);
}
});
};
export const fetchUserAction = async (action: string) => {
const { data } = await apiRequest.get<{ userAction: string }>("/api/v1/user-action", {
params: {
@@ -216,11 +234,16 @@ export const useLogoutUser = () =>
onSuccess: () => {
setAuthToken("");
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.setItem("publicKey", "");
localStorage.setItem("encryptedPrivateKey", "");
localStorage.setItem("iv", "");
localStorage.setItem("tag", "");
localStorage.setItem("PRIVATE_KEY", "");
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
}
});
+1 -10
View File
@@ -113,6 +113,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// eslint-disable-next-line prefer-const
const { workspaces, currentWorkspace } = useWorkspace();
const { orgs, currentOrg } = useOrganization();
const { user } = useUser();
const { subscription } = useSubscription();
const workspaceId = currentWorkspace?._id || "";
@@ -148,16 +149,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
try {
console.log("Logging out...");
await logout.mutateAsync();
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
router.push("/login");
} catch (error) {
console.error(error);
@@ -1,6 +1,6 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
export type GitRisks = {
export type IGitRisks = {
_id: string;
description: string;
startLine: string;
@@ -41,7 +41,7 @@ export type GitRisks = {
* Will create a new integration session and return it for the given org
* @returns
*/
const getRisksByOrganization = (oranizationId: string): Promise<GitRisks[]> =>
const getRisksByOrganization = (oranizationId: string): Promise<IGitRisks[]> =>
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${oranizationId}/risks`, {
method: "GET",
headers: {
@@ -18,4 +18,4 @@ export default function SettingsOrg() {
);
}
SettingsOrg.requireAuth = true;
SettingsOrg.requireAuth = true;
+20
View File
@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { NonePage } from "@app/views/Org/NonePage";
export default function NoneOrganization() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<NonePage />
</>
);
}
NoneOrganization.requireAuth = true;
+17
View File
@@ -0,0 +1,17 @@
import { NextRouter } from "next/router";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
export const navigateUserToOrg = async (router: NextRouter) => {
const userOrgs = await fetchOrganizations();
if (userOrgs.length > 0) {
// user is part of at least 1 org
const userOrg = userOrgs[0] && userOrgs[0]._id;
localStorage.setItem("orgData.id", userOrg);
router.push(`/org/${userOrg}/overview`);
} else {
// user is not part of any org
router.push("/org/none");
}
}
@@ -12,9 +12,10 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { navigateUserToOrg } from "../../Login.utils";
type Props = {
setStep: (step: number) => void;
email: string;
@@ -73,6 +74,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
email: email.toLowerCase(),
password
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
@@ -82,15 +84,14 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
setIsLoading(false);
return;
}
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
await navigateUserToOrg(router);
// case: login does not require MFA step
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
@@ -12,10 +12,10 @@ import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
import { Button } from "@app/components/v2";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { useSendMfaToken } from "@app/hooks/api/auth";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { AuthMethod } from "@app/hooks/api/users/types";
import { navigateUserToOrg } from "../../Login.utils";
// The style for the verification code input
const props = {
@@ -127,8 +127,6 @@ export const MFAStep = ({
if (isLoginSuccessful) {
setIsLoading(false);
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
createNotification({
@@ -144,7 +142,7 @@ export const MFAStep = ({
});
}
router.push(`/org/${userOrg}/overview`);
await navigateUserToOrg(router);
} else {
createNotification({
text: "Failed to log in",
@@ -10,9 +10,10 @@ import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { navigateUserToOrg } from "../../Login.utils";
type Props = {
providerAuthToken: string;
email: string;
@@ -92,8 +93,6 @@ export const PasswordStep = ({
}
// case: login does not require MFA step
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0]._id;
setIsLoading(false);
createNotification({
text: "Successfully logged in",
@@ -108,7 +107,7 @@ export const PasswordStep = ({
});
}
router.push(`/org/${userOrg}/overview`);
await navigateUserToOrg(router);
}
}
} catch (err) {
@@ -120,8 +119,6 @@ export const PasswordStep = ({
console.error(err);
}
};
return (
<form
@@ -0,0 +1,112 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent} from "@app/components/v2";
import { useCreateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
const schema = yup.object({
name: yup.string().required("Organization name is required"),
}).required();
export type FormData = yup.InferType<typeof schema>;
export const NonePage = () => {
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"createOrg",
] as const);
const { mutateAsync } = useCreateOrg();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
name: ""
}
});
useEffect(() => {
handlePopUpOpen("createOrg");
}, []);
const onFormSubmit = async ({ name }: FormData) => {
try {
const organization = await mutateAsync({
name
});
createNotification({
text: "Successfully created organization",
type: "success"
});
window.location.href = `/org/${organization._id}/overview`;
reset();
handlePopUpToggle("createOrg", false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to created organization",
type: "error"
});
}
}
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<Modal
isOpen={popUp?.createOrg?.isOpen}
>
<ModalContent
title="Create Organization"
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="Acme Corp"
/>
</FormControl>
)}
/>
<Button
className=""
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</form>
</ModalContent>
</Modal>
</div>
);
}
@@ -0,0 +1 @@
export { NonePage } from "./NonePage";
@@ -14,14 +14,14 @@ import {
} from "@app/components/v2";
import timeSince from "@app/ee/utilities/timeSince";
import getRisksByOrganization, {
GitRisks
IGitRisks
} from "@app/pages/api/secret-scanning/getRisksByOrganization";
import { RiskStatusSelection } from "./RiskStatusSelection";
export const SecretScanningLogsTable = () => {
const [isLoading, setIsLoading] = useState(false);
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
const [gitRisks, setGitRisks] = useState<IGitRisks[]>([]);
useEffect(() => {
const fetchRisks = async () => {
@@ -0,0 +1,81 @@
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 { 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 { 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"
});
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>
);
}
@@ -0,0 +1 @@
export { OrgDeleteSection } from "./OrgDeleteSection";
@@ -1,11 +1,24 @@
import { useOrganization, useUser } from "@app/context";
import { useGetOrgUsers } from "@app/hooks/api";
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);
return (
<div>
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{(membershipOrg && membershipOrg.role === "admin") && (
<OrgDeleteSection />
)}
</div>
);
};
@@ -18,7 +18,7 @@ export const OrgIncidentContactsSection = () => {
const permission = useOrgPermission();
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-4">
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>
@@ -53,7 +53,7 @@ export const OrgNameChangeSection = (): JSX.Element => {
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Name</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
@@ -0,0 +1,64 @@
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { useDeleteUser } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteAccountSection = () => {
const router = useRouter();
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteAccount"
] as const);
const { mutateAsync: deleteUserMutateAsync, isLoading } = useDeleteUser();
const handleDeleteAccountSubmit = async () => {
try {
await deleteUserMutateAsync();
createNotification({
text: "Successfully deleted account",
type: "success"
});
router.push("/login");
handlePopUpClose("deleteAccount");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete account",
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("deleteAccount")}
>
Delete my account
</Button>
<DeleteActionModal
isOpen={popUp.deleteAccount.isOpen}
title="Are you sure want to delete your account?"
subTitle="Permanently remove this account and all of its data. This action is not reversible, so please be careful."
onChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteAccountSubmit}
/>
</div>
);
}
@@ -0,0 +1 @@
export { DeleteAccountSection } from "./DeleteAccountSection";
@@ -1,4 +1,5 @@
import { ChangeLanguageSection } from "../ChangeLanguageSection";
import { DeleteAccountSection } from "../DeleteAccountSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
@@ -10,6 +11,7 @@ export const PersonalGeneralTab = () => {
<ChangeLanguageSection />
<SessionsSection />
<EmergencyKitSection />
<DeleteAccountSection />
</div>
);
}
@@ -1,10 +1,11 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import {
Button,
DeleteActionModal,
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@@ -13,78 +14,74 @@ import {
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useDeleteWorkspace } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteProjectSection = () => {
const { t } = useTranslation();
const router = useRouter();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
const [isDeleting, setIsDeleting] = useToggle();
const [deleteProjectInput, setDeleteProjectInput] = useState("");
const deleteWorkspace = useDeleteWorkspace();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteWorkspace"
] as const);
const onDeleteWorkspace = async () => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const [isDeleting, setIsDeleting] = useToggle();
const deleteWorkspace = useDeleteWorkspace();
const handleDeleteWorkspaceSubmit = async () => {
setIsDeleting.on();
try {
if (!currentWorkspace?._id) return;
await deleteWorkspace.mutateAsync({
workspaceID: currentWorkspace?._id
});
// redirect user to the org overview
router.push(`/org/${currentOrg?._id}/overview`);
createNotification({
text: "Successfully deleted workspace",
text: "Successfully deleted project",
type: "success"
});
} catch (error) {
console.error(error);
router.push(`/org/${currentOrg?._id}/overview`);
handlePopUpClose("deleteWorkspace");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete workspace",
text: "Failed to delete project",
type: "error"
});
} finally {
setIsDeleting.off();
}
};
}
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-red">
<p className="mb-3 text-xl font-semibold text-red">{t("settings.project.danger-zone")}</p>
<p className="text-gray-400 mb-8">{t("settings.project.danger-zone-note")}</p>
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
<FormControl
label={
<div className="mb-0.5 text-sm font-normal text-gray-400">
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
workspace
</div>
}
>
<Input
onChange={(e) => setDeleteProjectInput(e.target.value)}
value={deleteProjectInput}
placeholder="Type the project name to delete"
className="bg-mineshaft-800"
/>
</FormControl>
</div>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
colorSchema="danger"
onClick={onDeleteWorkspace}
isDisabled={!isAllowed || deleteProjectInput !== currentWorkspace?.name || isDeleting}
isLoading={isDeleting}
>
{t("settings.project.delete-project")}
</Button>
)}
</ProjectPermissionCan>
<p className="mt-3 ml-0.5 text-xs text-gray-500">
{t("settings.project.delete-project-note")}
</p>
<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>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
)}
</ProjectPermissionCan>
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
subTitle={`Permanently remove ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteWorkspaceSubmit}
/>
</div>
);
};