reset account UI

pull/1179/head
gabrielburnworth 2019-05-02 18:37:10 -07:00
parent 18ae05ca82
commit 8d9cf27e8a
10 changed files with 144 additions and 72 deletions

View File

@ -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();
});
});
});
});

View File

@ -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 }));

View File

@ -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({});
});
});

View File

@ -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({});
});
});

View File

@ -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<DeleteAccountProps, DeleteAccountState> {
state: DeleteAccountState = { password: "" };
/** Widget for permanently deleting large amounts of user data. */
export class DangerousDeleteWidget extends
React.Component<DangerousDeleteProps, DangerousDeleteState> {
state: DangerousDeleteState = { password: "" };
componentWillUnmount() {
this.setState({ password: "" });
}
onClick = () =>
this.props.dispatch(this.props.onClick({ password: this.state.password }));
render() {
return <Widget>
<WidgetHeader title="Delete Account" />
<WidgetHeader title={this.props.title} />
<WidgetBody>
<div>
{t(Content.ACCOUNT_DELETE_WARNING)}
{t(this.props.warning)}
<br /><br />
{t(Content.TYPE_PASSWORD_TO_DELETE)}
{t(this.props.confirmation)}
<br /><br />
</div>
<form>
@ -39,16 +35,15 @@ export class DeleteAccount extends
</Col>
<Col xs={8}>
<BlurablePassword
onCommit={(e) => {
this.setState({ password: e.currentTarget.value });
}} />
onCommit={e =>
this.setState({ password: e.currentTarget.value })} />
</Col>
<Col xs={4}>
<button
onClick={() => this.props.onClick(this.state.password)}
onClick={this.onClick}
className="red fb-button"
type="button" >
{t("Delete Account")}
type="button">
{t(this.props.title)}
</button>
</Col>
</Row>

View File

@ -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";

View File

@ -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<Props, State> {
.then(this.doSave, updateNO);
render() {
const deleteAcct =
(password: string) => this.props.dispatch(deleteUser({ password }));
return <Page className="account-page">
<Col xs={12} sm={6} smOffset={3}>
<Row>
@ -87,7 +86,20 @@ export class Account extends React.Component<Props, State> {
getConfigValue={this.props.getConfigValue} />
</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>
<ExportAccountPanel onClick={requestAccountExport} />

View File

@ -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;

View File

@ -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 */

View File

@ -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