diff --git a/app/scenes/UserDelete.tsx b/app/scenes/UserDelete.tsx index 48186b102..116b27b7e 100644 --- a/app/scenes/UserDelete.tsx +++ b/app/scenes/UserDelete.tsx @@ -1,65 +1,120 @@ import { observer } from "mobx-react"; import * as React from "react"; +import { useForm } from "react-hook-form"; import { useTranslation, Trans } from "react-i18next"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; +import { ReactHookWrappedInput as Input } from "~/components/Input"; import Modal from "~/components/Modal"; import Text from "~/components/Text"; +import env from "~/env"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; +type FormData = { + code: string; +}; + type Props = { onRequestClose: () => void; }; function UserDelete({ onRequestClose }: Props) { - const [isDeleting, setIsDeleting] = React.useState(false); + const [isWaitingCode, setWaitingCode] = React.useState(false); const { auth } = useStores(); const { showToast } = useToasts(); const { t } = useTranslation(); + const { register, handleSubmit: formHandleSubmit, formState } = useForm< + FormData + >(); - const handleSubmit = React.useCallback( + const handleRequestDelete = React.useCallback( async (ev: React.SyntheticEvent) => { ev.preventDefault(); - setIsDeleting(true); try { - await auth.deleteUser(); - auth.logout(); + await auth.requestDelete(); + setWaitingCode(true); } catch (error) { showToast(error.message, { type: "error", }); - } finally { - setIsDeleting(false); } }, [auth, showToast] ); + const handleSubmit = React.useCallback( + async (data: FormData) => { + try { + await auth.deleteUser(data); + auth.logout(); + } catch (error) { + showToast(error.message, { + type: "error", + }); + } + }, + [auth, showToast] + ); + + const inputProps = register("code", { + required: true, + }); + return ( -
- - - Are you sure? Deleting your account will destroy identifying data - associated with your user and cannot be undone. You will be - immediately logged out of Outline and all your API tokens will be - revoked. - - - - , - }} - /> - - + + {isWaitingCode ? ( + <> + + + A confirmation code has been sent to your email address, + please enter the code below to permanantly destroy your + account. + + + + , + }} + /> + + + + ) : ( + <> + + + Are you sure? Deleting your account will destroy identifying + data associated with your user and cannot be undone. You will + be immediately logged out of Outline and all your API tokens + will be revoked. + + + + )} + {env.EMAIL_ENABLED && !isWaitingCode ? ( + + ) : ( + + )}
diff --git a/app/stores/AuthStore.ts b/app/stores/AuthStore.ts index 0c4322f24..55d908a0e 100644 --- a/app/stores/AuthStore.ts +++ b/app/stores/AuthStore.ts @@ -199,8 +199,13 @@ export default class AuthStore { }; @action - deleteUser = async () => { - await client.post(`/users.delete`); + requestDelete = () => { + return client.post(`/users.requestDelete`); + }; + + @action + deleteUser = async (data: { code: string }) => { + await client.post(`/users.delete`, data); runInAction("AuthStore#updateUser", () => { this.user = null; this.team = null; diff --git a/server/emails/templates/ConfirmUserDeleteEmail.tsx b/server/emails/templates/ConfirmUserDeleteEmail.tsx new file mode 100644 index 000000000..0364aadc7 --- /dev/null +++ b/server/emails/templates/ConfirmUserDeleteEmail.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import BaseEmail from "./BaseEmail"; +import Body from "./components/Body"; +import CopyableCode from "./components/CopyableCode"; +import EmailTemplate from "./components/EmailLayout"; +import EmptySpace from "./components/EmptySpace"; +import Footer from "./components/Footer"; +import Header from "./components/Header"; +import Heading from "./components/Heading"; + +type Props = { + to: string; + deleteConfirmationCode: string; +}; + +/** + * Email sent to a user when they request to delete their account. + */ +export default class ConfirmUserDeleteEmail extends BaseEmail { + protected subject() { + return `Your account deletion request`; + } + + protected preview() { + return `Your requested account deletion code`; + } + + protected renderAsText({ deleteConfirmationCode }: Props): string { + return ` +You requested to permanantly delete your Outline account. Please enter the code below to confirm your account deletion. + +Code: ${deleteConfirmationCode} +`; + } + + protected render({ deleteConfirmationCode }: Props) { + return ( + +
+ + + Your account deletion request +

+ You requested to permanantly delete your Outline account. Please + enter the code below to confirm your account deletion. +

+ +

+ {deleteConfirmationCode} +

+ + +