commit
3f2a09a7a1
|
@ -1,6 +1,9 @@
|
||||||
|
jest.mock("../../toast_errors", () => ({ toastErrors: jest.fn() }));
|
||||||
|
|
||||||
import { API } from "../../api/api";
|
import { API } from "../../api/api";
|
||||||
import * as moxios from "moxios";
|
import * as moxios from "moxios";
|
||||||
import { deleteUser } from "../actions";
|
import { deleteUser, resetAccount } from "../actions";
|
||||||
|
import { toastErrors } from "../../toast_errors";
|
||||||
|
|
||||||
describe("deleteUser()", () => {
|
describe("deleteUser()", () => {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
@ -36,4 +39,52 @@ 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors while resetting an account", (done) => {
|
||||||
|
expect.assertions(3);
|
||||||
|
API.setBaseUrl("http://example.com:80");
|
||||||
|
const thunk = resetAccount({ password: "not 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: 422,
|
||||||
|
response: {}
|
||||||
|
}).then(resp => {
|
||||||
|
expect(window.alert).not.toHaveBeenCalled();
|
||||||
|
expect(toastErrors).toHaveBeenCalled();
|
||||||
|
expect(resp.config.url).toContain("api/device/reset");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { DeletionRequest } from "./interfaces";
|
||||||
import { Session } from "../session";
|
import { Session } from "../session";
|
||||||
import { UnsafeError } from "../interfaces";
|
import { UnsafeError } from "../interfaces";
|
||||||
import { toastErrors } from "../toast_errors";
|
import { toastErrors } from "../toast_errors";
|
||||||
|
import { t } from "../i18next_wrapper";
|
||||||
|
|
||||||
export function deleteUser(payload: DeletionRequest): Thunk {
|
export function deleteUser(payload: DeletionRequest): Thunk {
|
||||||
return (_, getState) => {
|
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 }));
|
||||||
|
|
|
@ -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("<DangerousDeleteWidget/>", () => {
|
||||||
|
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(<DangerousDeleteWidget {...p} />);
|
||||||
|
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<DangerousDeleteWidget>(
|
||||||
|
<DangerousDeleteWidget {...fakeProps()} />);
|
||||||
|
wrapper.find(BlurablePassword).simulate("commit", {
|
||||||
|
currentTarget: { value: "password" }
|
||||||
|
});
|
||||||
|
expect(wrapper.state().password).toEqual("password");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enters password", () => {
|
||||||
|
const wrapper = mount<DangerousDeleteWidget>(
|
||||||
|
<DangerousDeleteWidget {...fakeProps()} />);
|
||||||
|
wrapper.setState({ password: "password" });
|
||||||
|
wrapper.unmount();
|
||||||
|
expect(wrapper).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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("<DeleteAccount/>", () => {
|
|
||||||
const fakeProps = (): DeleteAccountProps => ({
|
|
||||||
onClick: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
it("executes account deletion", () => {
|
|
||||||
const p = fakeProps();
|
|
||||||
const wrapper = mount(<DeleteAccount {...p} />);
|
|
||||||
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<DeleteAccount>(<DeleteAccount {...fakeProps()} />);
|
|
||||||
wrapper.find(BlurablePassword).simulate("commit", {
|
|
||||||
currentTarget: { value: "password" }
|
|
||||||
});
|
|
||||||
expect(wrapper.state().password).toEqual("password");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("enters password", () => {
|
|
||||||
const wrapper = mount<DeleteAccount>(<DeleteAccount {...fakeProps()} />);
|
|
||||||
wrapper.setState({ password: "password" });
|
|
||||||
wrapper.unmount();
|
|
||||||
expect(wrapper).toEqual({});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,33 +1,29 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { Widget, WidgetHeader, WidgetBody, Col, Row } from "../../ui";
|
||||||
import {
|
import { DangerousDeleteProps, DangerousDeleteState } from "../interfaces";
|
||||||
Widget,
|
|
||||||
WidgetHeader,
|
|
||||||
WidgetBody,
|
|
||||||
Col,
|
|
||||||
Row
|
|
||||||
} from "../../ui/index";
|
|
||||||
import { DeleteAccountProps, DeleteAccountState } from "../interfaces";
|
|
||||||
import { Content } from "../../constants";
|
|
||||||
import { BlurablePassword } from "../../ui/blurable_password";
|
import { BlurablePassword } from "../../ui/blurable_password";
|
||||||
import { t } from "../../i18next_wrapper";
|
import { t } from "../../i18next_wrapper";
|
||||||
|
|
||||||
export class DeleteAccount extends
|
/** Widget for permanently deleting large amounts of user data. */
|
||||||
React.Component<DeleteAccountProps, DeleteAccountState> {
|
export class DangerousDeleteWidget extends
|
||||||
state: DeleteAccountState = { password: "" };
|
React.Component<DangerousDeleteProps, DangerousDeleteState> {
|
||||||
|
state: DangerousDeleteState = { password: "" };
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.setState({ password: "" });
|
this.setState({ password: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClick = () =>
|
||||||
|
this.props.dispatch(this.props.onClick({ password: this.state.password }));
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Widget>
|
return <Widget>
|
||||||
<WidgetHeader title="Delete Account" />
|
<WidgetHeader title={this.props.title} />
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<div>
|
<div>
|
||||||
{t(Content.ACCOUNT_DELETE_WARNING)}
|
{t(this.props.warning)}
|
||||||
<br /><br />
|
<br /><br />
|
||||||
{t(Content.TYPE_PASSWORD_TO_DELETE)}
|
{t(this.props.confirmation)}
|
||||||
<br /><br />
|
<br /><br />
|
||||||
</div>
|
</div>
|
||||||
<form>
|
<form>
|
||||||
|
@ -39,16 +35,15 @@ export class DeleteAccount extends
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={8}>
|
<Col xs={8}>
|
||||||
<BlurablePassword
|
<BlurablePassword
|
||||||
onCommit={(e) => {
|
onCommit={e =>
|
||||||
this.setState({ password: e.currentTarget.value });
|
this.setState({ password: e.currentTarget.value })} />
|
||||||
}} />
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={4}>
|
<Col xs={4}>
|
||||||
<button
|
<button
|
||||||
onClick={() => this.props.onClick(this.state.password)}
|
onClick={this.onClick}
|
||||||
className="red fb-button"
|
className="red fb-button"
|
||||||
type="button" >
|
type="button">
|
||||||
{t("Delete Account")}
|
{t(this.props.title)}
|
||||||
</button>
|
</button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./change_password";
|
export * from "./change_password";
|
||||||
export * from "./delete_account";
|
export * from "./export_account_panel";
|
||||||
|
export * from "./dangerous_delete_widget";
|
||||||
export * from "./settings";
|
export * from "./settings";
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Settings, DeleteAccount, ChangePassword } from "./components";
|
import {
|
||||||
|
Settings, ChangePassword, ExportAccountPanel, DangerousDeleteWidget
|
||||||
|
} from "./components";
|
||||||
import { Props } from "./interfaces";
|
import { Props } from "./interfaces";
|
||||||
import { Page, Row, Col } from "../ui/index";
|
import { Page, Row, Col } from "../ui";
|
||||||
import { mapStateToProps } from "./state_to_props";
|
import { mapStateToProps } from "./state_to_props";
|
||||||
import { User } from "../auth/interfaces";
|
import { User } from "../auth/interfaces";
|
||||||
import { edit, save } from "../api/crud";
|
import { edit, save } from "../api/crud";
|
||||||
import { updateNO } from "../resources/actions";
|
import { updateNO } from "../resources/actions";
|
||||||
import { deleteUser } from "./actions";
|
import { deleteUser, resetAccount } from "./actions";
|
||||||
import { success } from "farmbot-toastr/dist";
|
import { success } from "farmbot-toastr/dist";
|
||||||
import { LabsFeatures } from "./labs/labs_features";
|
import { LabsFeatures } from "./labs/labs_features";
|
||||||
import { ExportAccountPanel } from "./components/export_account_panel";
|
|
||||||
import { requestAccountExport } from "./request_account_export";
|
import { requestAccountExport } from "./request_account_export";
|
||||||
import { DevWidget } from "./dev/dev_widget";
|
import { DevWidget } from "./dev/dev_widget";
|
||||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||||
import { DevMode } from "./dev/dev_mode";
|
import { DevMode } from "./dev/dev_mode";
|
||||||
import { t } from "../i18next_wrapper";
|
import { t } from "../i18next_wrapper";
|
||||||
|
import { Content } from "../constants";
|
||||||
|
|
||||||
const KEYS: (keyof User)[] = ["id", "name", "email", "created_at", "updated_at"];
|
const KEYS: (keyof User)[] = ["id", "name", "email", "created_at", "updated_at"];
|
||||||
|
|
||||||
|
@ -67,9 +69,6 @@ export class Account extends React.Component<Props, State> {
|
||||||
.then(this.doSave, updateNO);
|
.then(this.doSave, updateNO);
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const deleteAcct =
|
|
||||||
(password: string) => this.props.dispatch(deleteUser({ password }));
|
|
||||||
|
|
||||||
return <Page className="account-page">
|
return <Page className="account-page">
|
||||||
<Col xs={12} sm={6} smOffset={3}>
|
<Col xs={12} sm={6} smOffset={3}>
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -87,7 +86,20 @@ export class Account extends React.Component<Props, State> {
|
||||||
getConfigValue={this.props.getConfigValue} />
|
getConfigValue={this.props.getConfigValue} />
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<DeleteAccount onClick={deleteAcct} />
|
<DangerousDeleteWidget
|
||||||
|
title={t("Reset Account")}
|
||||||
|
warning={t(Content.ACCOUNT_RESET_WARNING)}
|
||||||
|
confirmation={t(Content.TYPE_PASSWORD_TO_RESET)}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
onClick={resetAccount} />
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<DangerousDeleteWidget
|
||||||
|
title={t("Delete Account")}
|
||||||
|
warning={t(Content.ACCOUNT_DELETE_WARNING)}
|
||||||
|
confirmation={t(Content.TYPE_PASSWORD_TO_DELETE)}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
onClick={deleteUser} />
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<ExportAccountPanel onClick={requestAccountExport} />
|
<ExportAccountPanel onClick={requestAccountExport} />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { User } from "../auth/interfaces";
|
import { User } from "../auth/interfaces";
|
||||||
import { TaggedUser } from "farmbot";
|
import { TaggedUser } from "farmbot";
|
||||||
import { GetWebAppConfigValue } from "../config_storage/actions";
|
import { GetWebAppConfigValue } from "../config_storage/actions";
|
||||||
|
import { Thunk } from "../redux/interfaces";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
user: TaggedUser;
|
user: TaggedUser;
|
||||||
|
@ -20,13 +21,15 @@ export interface DeletionRequest {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteAccountProps {
|
export interface DangerousDeleteProps {
|
||||||
onClick(pw: string): void;
|
title: string;
|
||||||
|
warning: string;
|
||||||
|
confirmation: string;
|
||||||
|
dispatch: Function;
|
||||||
|
onClick(payload: DeletionRequest): Thunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteAccountState {
|
export interface DangerousDeleteState extends DeletionRequest { }
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SettingsPropTypes {
|
export interface SettingsPropTypes {
|
||||||
user: TaggedUser;
|
user: TaggedUser;
|
||||||
|
|
|
@ -89,6 +89,8 @@ export class API {
|
||||||
get passwordResetPath() { return `${this.baseUrl}/api/password_resets/`; }
|
get passwordResetPath() { return `${this.baseUrl}/api/password_resets/`; }
|
||||||
/** /api/device/ */
|
/** /api/device/ */
|
||||||
get devicePath() { return `${this.baseUrl}/api/device/`; }
|
get devicePath() { return `${this.baseUrl}/api/device/`; }
|
||||||
|
/** /api/device/reset */
|
||||||
|
get accountResetPath() { return `${this.devicePath}reset`; }
|
||||||
/** /api/users/ */
|
/** /api/users/ */
|
||||||
get usersPath() { return `${this.baseUrl}/api/users/`; }
|
get usersPath() { return `${this.baseUrl}/api/users/`; }
|
||||||
/** /api/users/control_certificate */
|
/** /api/users/control_certificate */
|
||||||
|
|
|
@ -366,6 +366,23 @@ export namespace Content {
|
||||||
allowing you to configure it with the updated credentials. You will also be
|
allowing you to configure it with the updated credentials. You will also be
|
||||||
logged out of other browser sessions. Continue?`);
|
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 =
|
export const ACCOUNT_DELETE_WARNING =
|
||||||
trim(`WARNING! Deleting your account will permanently delete all of your
|
trim(`WARNING! Deleting your account will permanently delete all of your
|
||||||
Sequences, Regimens, Events, and Farm Designer data. Upon deleting your
|
Sequences, Regimens, Events, and Farm Designer data. Upon deleting your
|
||||||
|
|
Loading…
Reference in New Issue