commit
3f2a09a7a1
|
@ -1,6 +1,9 @@
|
|||
jest.mock("../../toast_errors", () => ({ toastErrors: jest.fn() }));
|
||||
|
||||
import { API } from "../../api/api";
|
||||
import * as moxios from "moxios";
|
||||
import { deleteUser } from "../actions";
|
||||
import { deleteUser, resetAccount } from "../actions";
|
||||
import { toastErrors } from "../../toast_errors";
|
||||
|
||||
describe("deleteUser()", () => {
|
||||
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 { 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 }));
|
||||
|
|
|
@ -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 {
|
||||
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>
|
|
@ -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";
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue