diff --git a/frontend/account/__tests__/actions_test.ts b/frontend/account/__tests__/actions_test.ts index 1da54915a..6e5881853 100644 --- a/frontend/account/__tests__/actions_test.ts +++ b/frontend/account/__tests__/actions_test.ts @@ -1,6 +1,6 @@ import { API } from "../../api/api"; import * as moxios from "moxios"; -import { deleteUser } from "../actions"; +import { deleteUser, resetAccount } from "../actions"; describe("deleteUser()", () => { beforeEach(function () { @@ -36,4 +36,28 @@ describe("deleteUser()", () => { }); }); }); + + it("resets the account", (done) => { + expect.assertions(3); + API.setBaseUrl("http://example.com:80"); + const thunk = resetAccount({ password: "Foo!" }); + const dispatch = jest.fn(); + const getState = jest.fn(); + getState.mockImplementation(() => ({ auth: {} })); + window.alert = jest.fn(); + thunk(dispatch, getState); + + moxios.wait(function () { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: {} + }).then(function (resp) { + expect(window.alert).toHaveBeenCalled(); + expect(resp.config.url).toContain("api/device/reset"); + expect(resp.config.method).toBe("post"); + done(); + }); + }); + }); }); diff --git a/frontend/account/actions.ts b/frontend/account/actions.ts index 16ff72aa4..020539c16 100644 --- a/frontend/account/actions.ts +++ b/frontend/account/actions.ts @@ -5,6 +5,7 @@ import { DeletionRequest } from "./interfaces"; import { Session } from "../session"; import { UnsafeError } from "../interfaces"; import { toastErrors } from "../toast_errors"; +import { t } from "../i18next_wrapper"; export function deleteUser(payload: DeletionRequest): Thunk { return (_, getState) => { @@ -24,3 +25,13 @@ export function deleteUser(payload: DeletionRequest): Thunk { }); }; } + +export const resetAccount = (payload: DeletionRequest): Thunk => + (_, getState) => + getState().auth && axios({ + method: "post", + url: API.current.accountResetPath, + data: payload, + }) + .then(() => alert(t("Account has been reset."))) + .catch((err: UnsafeError) => toastErrors({ err })); diff --git a/frontend/account/components/__tests__/dangerous_delete_widget_test.tsx b/frontend/account/components/__tests__/dangerous_delete_widget_test.tsx new file mode 100644 index 000000000..0f6c5eefd --- /dev/null +++ b/frontend/account/components/__tests__/dangerous_delete_widget_test.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { DangerousDeleteWidget } from "../dangerous_delete_widget"; +import { mount, shallow } from "enzyme"; +import { DangerousDeleteProps } from "../../interfaces"; +import { BlurablePassword } from "../../../ui/blurable_password"; + +describe("", () => { + const fakeProps = (): DangerousDeleteProps => ({ + title: "Delete something important", + warning: "This will remove data.", + confirmation: "Are you sure?", + dispatch: jest.fn(), + onClick: jest.fn(), + }); + + it("executes deletion", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.setState({ password: "123" }); + wrapper.find("button.red").last().simulate("click"); + expect(p.onClick).toHaveBeenCalledTimes(1); + expect(p.onClick).toHaveBeenCalledWith({ password: "123" }); + expect(p.dispatch).toHaveBeenCalled(); + }); + + it("enters password", () => { + const wrapper = shallow( + ); + wrapper.find(BlurablePassword).simulate("commit", { + currentTarget: { value: "password" } + }); + expect(wrapper.state().password).toEqual("password"); + }); + + it("enters password", () => { + const wrapper = mount( + ); + wrapper.setState({ password: "password" }); + wrapper.unmount(); + expect(wrapper).toEqual({}); + }); +}); diff --git a/frontend/account/components/__tests__/delete_account_test.tsx b/frontend/account/components/__tests__/delete_account_test.tsx deleted file mode 100644 index d835889fa..000000000 --- a/frontend/account/components/__tests__/delete_account_test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from "react"; -import { DeleteAccount } from "../delete_account"; -import { mount, shallow } from "enzyme"; -import { DeleteAccountProps } from "../../interfaces"; -import { BlurablePassword } from "../../../ui/blurable_password"; - -describe("", () => { - const fakeProps = (): DeleteAccountProps => ({ - onClick: jest.fn(), - }); - - it("executes account deletion", () => { - const p = fakeProps(); - const wrapper = mount(); - wrapper.setState({ password: "123" }); - wrapper.find("button.red").last().simulate("click"); - expect(p.onClick).toHaveBeenCalledTimes(1); - expect(p.onClick).toHaveBeenCalledWith("123"); - }); - - it("enters password", () => { - const wrapper = shallow(); - wrapper.find(BlurablePassword).simulate("commit", { - currentTarget: { value: "password" } - }); - expect(wrapper.state().password).toEqual("password"); - }); - - it("enters password", () => { - const wrapper = mount(); - wrapper.setState({ password: "password" }); - wrapper.unmount(); - expect(wrapper).toEqual({}); - }); -}); diff --git a/frontend/account/components/delete_account.tsx b/frontend/account/components/dangerous_delete_widget.tsx similarity index 53% rename from frontend/account/components/delete_account.tsx rename to frontend/account/components/dangerous_delete_widget.tsx index 68225b4f0..635c050e9 100644 --- a/frontend/account/components/delete_account.tsx +++ b/frontend/account/components/dangerous_delete_widget.tsx @@ -1,33 +1,29 @@ import * as React from "react"; - -import { - Widget, - WidgetHeader, - WidgetBody, - Col, - Row -} from "../../ui/index"; -import { DeleteAccountProps, DeleteAccountState } from "../interfaces"; -import { Content } from "../../constants"; +import { Widget, WidgetHeader, WidgetBody, Col, Row } from "../../ui"; +import { DangerousDeleteProps, DangerousDeleteState } from "../interfaces"; import { BlurablePassword } from "../../ui/blurable_password"; import { t } from "../../i18next_wrapper"; -export class DeleteAccount extends - React.Component { - state: DeleteAccountState = { password: "" }; +/** Widget for permanently deleting large amounts of user data. */ +export class DangerousDeleteWidget extends + React.Component { + state: DangerousDeleteState = { password: "" }; componentWillUnmount() { this.setState({ password: "" }); } + onClick = () => + this.props.dispatch(this.props.onClick({ password: this.state.password })); + render() { return - +
- {t(Content.ACCOUNT_DELETE_WARNING)} + {t(this.props.warning)}

- {t(Content.TYPE_PASSWORD_TO_DELETE)} + {t(this.props.confirmation)}

@@ -39,16 +35,15 @@ export class DeleteAccount extends { - this.setState({ password: e.currentTarget.value }); - }} /> + onCommit={e => + this.setState({ password: e.currentTarget.value })} /> diff --git a/frontend/account/components/index.ts b/frontend/account/components/index.ts index ed6f8099f..17ac37da6 100644 --- a/frontend/account/components/index.ts +++ b/frontend/account/components/index.ts @@ -1,3 +1,4 @@ export * from "./change_password"; -export * from "./delete_account"; +export * from "./export_account_panel"; +export * from "./dangerous_delete_widget"; export * from "./settings"; diff --git a/frontend/account/index.tsx b/frontend/account/index.tsx index d5fea49ea..a6f2a87b4 100644 --- a/frontend/account/index.tsx +++ b/frontend/account/index.tsx @@ -1,21 +1,23 @@ import * as React from "react"; import { connect } from "react-redux"; -import { Settings, DeleteAccount, ChangePassword } from "./components"; +import { + Settings, ChangePassword, ExportAccountPanel, DangerousDeleteWidget +} from "./components"; import { Props } from "./interfaces"; -import { Page, Row, Col } from "../ui/index"; +import { Page, Row, Col } from "../ui"; import { mapStateToProps } from "./state_to_props"; import { User } from "../auth/interfaces"; import { edit, save } from "../api/crud"; import { updateNO } from "../resources/actions"; -import { deleteUser } from "./actions"; +import { deleteUser, resetAccount } from "./actions"; import { success } from "farmbot-toastr/dist"; import { LabsFeatures } from "./labs/labs_features"; -import { ExportAccountPanel } from "./components/export_account_panel"; import { requestAccountExport } from "./request_account_export"; import { DevWidget } from "./dev/dev_widget"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { DevMode } from "./dev/dev_mode"; import { t } from "../i18next_wrapper"; +import { Content } from "../constants"; const KEYS: (keyof User)[] = ["id", "name", "email", "created_at", "updated_at"]; @@ -67,9 +69,6 @@ export class Account extends React.Component { .then(this.doSave, updateNO); render() { - const deleteAcct = - (password: string) => this.props.dispatch(deleteUser({ password })); - return @@ -87,7 +86,20 @@ export class Account extends React.Component { getConfigValue={this.props.getConfigValue} /> - + + + + diff --git a/frontend/account/interfaces.ts b/frontend/account/interfaces.ts index 3569493c9..89a4a4a77 100644 --- a/frontend/account/interfaces.ts +++ b/frontend/account/interfaces.ts @@ -1,6 +1,7 @@ import { User } from "../auth/interfaces"; import { TaggedUser } from "farmbot"; import { GetWebAppConfigValue } from "../config_storage/actions"; +import { Thunk } from "../redux/interfaces"; export interface Props { user: TaggedUser; @@ -20,13 +21,15 @@ export interface DeletionRequest { password: string; } -export interface DeleteAccountProps { - onClick(pw: string): void; +export interface DangerousDeleteProps { + title: string; + warning: string; + confirmation: string; + dispatch: Function; + onClick(payload: DeletionRequest): Thunk; } -export interface DeleteAccountState { - password: string; -} +export interface DangerousDeleteState extends DeletionRequest { } export interface SettingsPropTypes { user: TaggedUser; diff --git a/frontend/api/api.ts b/frontend/api/api.ts index 93f0cb8fb..ba9898858 100644 --- a/frontend/api/api.ts +++ b/frontend/api/api.ts @@ -89,6 +89,8 @@ export class API { get passwordResetPath() { return `${this.baseUrl}/api/password_resets/`; } /** /api/device/ */ get devicePath() { return `${this.baseUrl}/api/device/`; } + /** /api/device/reset */ + get accountResetPath() { return `${this.devicePath}reset`; } /** /api/users/ */ get usersPath() { return `${this.baseUrl}/api/users/`; } /** /api/users/control_certificate */ diff --git a/frontend/constants.ts b/frontend/constants.ts index d3ea1bdc6..55de4ae10 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -366,6 +366,23 @@ export namespace Content { allowing you to configure it with the updated credentials. You will also be logged out of other browser sessions. Continue?`); + export const ACCOUNT_RESET_WARNING = + trim(`WARNING! Resetting your account will permanently delete all of your + Sequences, Regimens, Events, Tools, Logs, and Farm Designer data. + All app settings and device settings will be reset to default values. + This is useful if you want to delete all data to start from scratch + while avoiding having to fully delete your account, re-signup, and + re-configure your FarmBot. Note that when you sync (or auto-sync) + after resetting your account, your FarmBot will delete all of its + stored Sequences, etc, because your account will no longer have any + of these resources until you create new ones. Furthermore, upon reset + any customized device settings will be immediately overwritten with + the default values downloaded from the reset web app account.`); + + export const TYPE_PASSWORD_TO_RESET = + trim(`If you are sure you want to reset your account, type in + your password below to continue.`); + export const ACCOUNT_DELETE_WARNING = trim(`WARNING! Deleting your account will permanently delete all of your Sequences, Regimens, Events, and Farm Designer data. Upon deleting your