Move password reset form to separate route, view (#1390)
authorAlec Armbruster <35377827+alectrocute@users.noreply.github.com>
Mon, 26 Jun 2023 18:54:38 +0000 (14:54 -0400)
committerJay Sitter <jay@jaysitter.com>
Mon, 26 Jun 2023 22:30:31 +0000 (18:30 -0400)
* rework password reset form

* make self-suggested changes

* cleaning

* validate in handlePasswordReset as well

* update submodule

* partially make suggested changes

* make suggested changes

* resolve merge conflicts

* resolve merge conflicts

* resolve merge conflicts

---------

Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
src/server/handlers/robots-handler.ts
src/shared/components/home/login-reset.tsx [new file with mode: 0644]
src/shared/components/home/login.tsx
src/shared/routes.ts

index 7271095c1575987602fa1e0cd6959af5b99bb1bb..80678aa08ada73b4c1906633ca17aeb567d16c68 100644 (file)
@@ -5,6 +5,7 @@ export default async ({ res }: { res: Response }) => {
 
   res.send(`User-Agent: *
   Disallow: /login
+  Disallow: /login_reset
   Disallow: /settings
   Disallow: /create_community
   Disallow: /create_post
diff --git a/src/shared/components/home/login-reset.tsx b/src/shared/components/home/login-reset.tsx
new file mode 100644 (file)
index 0000000..c96255d
--- /dev/null
@@ -0,0 +1,138 @@
+import { setIsoData } from "@utils/app";
+import { capitalizeFirstLetter, validEmail } from "@utils/helpers";
+import { Component, linkEvent } from "inferno";
+import { GetSiteResponse } from "lemmy-js-client";
+import { HttpService, I18NextService, UserService } from "../../services";
+import { toast } from "../../toast";
+import { HtmlTags } from "../common/html-tags";
+import { Spinner } from "../common/icon";
+
+interface State {
+  form: {
+    email: string;
+    loading: boolean;
+  };
+  siteRes: GetSiteResponse;
+}
+
+export class LoginReset extends Component<any, State> {
+  private isoData = setIsoData(this.context);
+
+  state: State = {
+    form: {
+      email: "",
+      loading: false,
+    },
+    siteRes: this.isoData.site_res,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+  }
+
+  componentDidMount() {
+    if (UserService.Instance.myUserInfo) {
+      this.context.router.history.push("/");
+    }
+  }
+
+  get documentTitle(): string {
+    return `${capitalizeFirstLetter(
+      I18NextService.i18n.t("forgot_password")
+    )} - ${this.state.siteRes.site_view.site.name}`;
+  }
+
+  render() {
+    return (
+      <div className="container-lg">
+        <HtmlTags
+          title={this.documentTitle}
+          path={this.context.router.route.match.url}
+        />
+        <div className="col-12 col-lg-6 col-md-8 m-auto">
+          {this.loginResetForm()}
+        </div>
+      </div>
+    );
+  }
+
+  loginResetForm() {
+    return (
+      <form onSubmit={linkEvent(this, this.handlePasswordReset)}>
+        <h5>
+          {capitalizeFirstLetter(I18NextService.i18n.t("forgot_password"))}
+        </h5>
+
+        <div className="form-group row">
+          <label className="col-form-label">
+            {I18NextService.i18n.t("no_password_reset")}
+          </label>
+        </div>
+
+        <div className="form-group row mt-2">
+          <label
+            className="col-sm-2 col-form-label"
+            htmlFor="login-reset-email"
+          >
+            {I18NextService.i18n.t("email")}
+          </label>
+
+          <div className="col-sm-10">
+            <input
+              type="text"
+              className="form-control"
+              id="login-reset-email"
+              value={this.state.form.email}
+              onInput={linkEvent(this, this.handleEmailInputChange)}
+              autoComplete="email"
+              required
+              minLength={3}
+            />
+          </div>
+        </div>
+
+        <div className="form-group row mt-3">
+          <div className="col-sm-10">
+            <button
+              type="button"
+              onClick={linkEvent(this, this.handlePasswordReset)}
+              className="btn btn-secondary"
+              disabled={
+                !validEmail(this.state.form.email) || this.state.form.loading
+              }
+            >
+              {this.state.form.loading ? (
+                <Spinner />
+              ) : (
+                I18NextService.i18n.t("reset_password")
+              )}
+            </button>
+          </div>
+        </div>
+      </form>
+    );
+  }
+
+  handleEmailInputChange(i: LoginReset, event: any) {
+    i.setState(s => ((s.form.email = event.target.value.trim()), s));
+  }
+
+  async handlePasswordReset(i: LoginReset, event: any) {
+    event.preventDefault();
+
+    const email = i.state.form.email;
+
+    if (email && validEmail(email)) {
+      i.setState(s => ((s.form.loading = true), s));
+
+      const res = await HttpService.client.passwordReset({ email });
+
+      if (res.state == "success") {
+        toast(I18NextService.i18n.t("reset_password_mail_sent"));
+        i.context.router.history.push("/login");
+      }
+
+      i.setState(s => ((s.form.loading = false), s));
+    }
+  }
+}
index 4dd6466577f0180f664f8a43078fe0e427a6a0a7..397288e888dbb9a2d0b6c5b1b7db4503d92c9bd3 100644 (file)
@@ -1,7 +1,7 @@
 import { myAuth, setIsoData } from "@utils/app";
 import { isBrowser } from "@utils/browser";
-import { validEmail } from "@utils/helpers";
 import { Component, linkEvent } from "inferno";
+import { NavLink } from "inferno-router";
 import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
 import { I18NextService, UserService } from "../../services";
 import { HttpService, RequestState } from "../../services/HttpService";
@@ -105,18 +105,12 @@ export class Login extends Component<any, State> {
                 required
                 maxLength={60}
               />
-              <button
-                type="button"
-                onClick={linkEvent(this, this.handlePasswordReset)}
-                className="btn p-0 btn-link d-inline-block float-right text-muted small fw-bold pointer-events not-allowed"
-                disabled={
-                  !!this.state.form.username_or_email &&
-                  !validEmail(this.state.form.username_or_email)
-                }
-                title={I18NextService.i18n.t("no_password_reset")}
+              <NavLink
+                className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
+                to="/login_reset"
               >
                 {I18NextService.i18n.t("forgot_password")}
-              </button>
+              </NavLink>
             </div>
           </div>
           {this.state.showTotp && (
@@ -214,15 +208,4 @@ export class Login extends Component<any, State> {
     i.state.form.password = event.target.value;
     i.setState(i.state);
   }
-
-  async handlePasswordReset(i: Login, event: any) {
-    event.preventDefault();
-    const email = i.state.form.username_or_email;
-    if (email) {
-      const res = await HttpService.client.passwordReset({ email });
-      if (res.state == "success") {
-        toast(I18NextService.i18n.t("reset_password_mail_sent"));
-      }
-    }
-  }
 }
index 6e3ed4987edc7062e6c26d76134427b6d1ad619c..01325afa32e0723cfabeaa948ce49f7690404021 100644 (file)
@@ -7,6 +7,7 @@ import { Home } from "./components/home/home";
 import { Instances } from "./components/home/instances";
 import { Legal } from "./components/home/legal";
 import { Login } from "./components/home/login";
+import { LoginReset } from "./components/home/login-reset";
 import { Setup } from "./components/home/setup";
 import { Signup } from "./components/home/signup";
 import { Modlog } from "./components/modlog";
@@ -38,6 +39,10 @@ export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
     path: `/login`,
     component: Login,
   },
+  {
+    path: `/login_reset`,
+    component: LoginReset,
+  },
   {
     path: `/signup`,
     component: Signup,