]> Untitled Git - lemmy-ui.git/commitdiff
Adding 2FA support. Fixes #938 (#939)
authorDessalines <dessalines@users.noreply.github.com>
Thu, 2 Mar 2023 23:30:38 +0000 (18:30 -0500)
committerGitHub <noreply@github.com>
Thu, 2 Mar 2023 23:30:38 +0000 (18:30 -0500)
* Adding 2FA support. Fixes #938

* Updating totp_2fa names.

package.json
src/shared/components/home/login.tsx
src/shared/components/person/settings.tsx
src/shared/utils.ts
yarn.lock

index 839a3d88e4bd78eec23c197e0e839a163273f0b7..26f10e9c71a1b2559aa5a56687a52c6871fe415d 100644 (file)
@@ -45,7 +45,7 @@
     "inferno-server": "^8.0.6",
     "isomorphic-cookie": "^1.2.4",
     "jwt-decode": "^3.1.2",
-    "lemmy-js-client": "0.17.2-rc.1",
+    "lemmy-js-client": "0.17.2-rc.3",
     "markdown-it": "^13.0.1",
     "markdown-it-container": "^3.0.0",
     "markdown-it-footnote": "^3.0.3",
index 7cdde927684f63b9868adbac77221a941a7b68f2..dea64842c098257349109ca502ef7073750c3ab3 100644 (file)
@@ -26,8 +26,10 @@ interface State {
   form: {
     username_or_email?: string;
     password?: string;
+    totp_2fa_token?: string;
   };
   loginLoading: boolean;
+  showTotp: boolean;
   siteRes: GetSiteResponse;
 }
 
@@ -38,6 +40,7 @@ export class Login extends Component<any, State> {
   state: State = {
     form: {},
     loginLoading: false,
+    showTotp: false,
     siteRes: this.isoData.site_res,
   };
 
@@ -141,6 +144,28 @@ export class Login extends Component<any, State> {
               </button>
             </div>
           </div>
+          {this.state.showTotp && (
+            <div className="form-group row">
+              <label
+                className="col-sm-6 col-form-label"
+                htmlFor="login-totp-token"
+              >
+                {i18n.t("two_factor_token")}
+              </label>
+              <div className="col-sm-6">
+                <input
+                  type="number"
+                  inputMode="numeric"
+                  className="form-control"
+                  id="login-totp-token"
+                  pattern="[0-9]*"
+                  autoComplete="one-time-code"
+                  value={this.state.form.totp_2fa_token}
+                  onInput={linkEvent(this, this.handleLoginTotpChange)}
+                />
+              </div>
+            </div>
+          )}
           <div className="form-group row">
             <div className="col-sm-10">
               <button type="submit" className="btn btn-secondary">
@@ -159,10 +184,12 @@ export class Login extends Component<any, State> {
     let lForm = i.state.form;
     let username_or_email = lForm.username_or_email;
     let password = lForm.password;
+    let totp_2fa_token = lForm.totp_2fa_token;
     if (username_or_email && password) {
       let form: LoginI = {
         username_or_email,
         password,
+        totp_2fa_token,
       };
       WebSocketService.Instance.send(wsClient.login(form));
     }
@@ -173,6 +200,11 @@ export class Login extends Component<any, State> {
     i.setState(i.state);
   }
 
+  handleLoginTotpChange(i: Login, event: any) {
+    i.state.form.totp_2fa_token = event.target.value;
+    i.setState(i.state);
+  }
+
   handleLoginPasswordChange(i: Login, event: any) {
     i.state.form.password = event.target.value;
     i.setState(i.state);
@@ -191,9 +223,16 @@ export class Login extends Component<any, State> {
     let op = wsUserOp(msg);
     console.log(msg);
     if (msg.error) {
-      toast(i18n.t(msg.error), "danger");
-      this.setState({ form: {} });
-      return;
+      // If the error comes back that the token is missing, show the TOTP field
+      if (msg.error == "missing_totp_token") {
+        this.setState({ showTotp: true, loginLoading: false });
+        toast(i18n.t("enter_two_factor_code"));
+        return;
+      } else {
+        toast(i18n.t(msg.error), "danger");
+        this.setState({ form: {}, loginLoading: false });
+        return;
+      }
     } else {
       if (op == UserOperation.Login) {
         let data = wsJsonToRes<LoginResponse>(msg);
index 734ae7120d55e57bc66f71fe9164b205f766820f..a349d1cdc673187dde4f4bd704a032a1fb508633 100644 (file)
@@ -86,6 +86,7 @@ interface SettingsState {
     show_read_posts?: boolean;
     show_new_post_notifs?: boolean;
     discussion_languages?: number[];
+    generate_totp_2fa?: boolean;
   };
   changePasswordForm: {
     new_password?: string;
@@ -789,6 +790,7 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
+          {this.totpSection()}
           <div className="form-group">
             <button type="submit" className="btn btn-block btn-secondary mr-4">
               {this.state.saveUserSettingsLoading ? (
@@ -853,6 +855,58 @@ export class Settings extends Component<any, SettingsState> {
     );
   }
 
+  totpSection() {
+    let totpUrl =
+      UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
+
+    return (
+      <>
+        {!totpUrl && (
+          <div className="form-group">
+            <div className="form-check">
+              <input
+                className="form-check-input"
+                id="user-generate-totp"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.generate_totp_2fa}
+                onChange={linkEvent(this, this.handleGenerateTotp)}
+              />
+              <label className="form-check-label" htmlFor="user-generate-totp">
+                {i18n.t("set_up_two_factor")}
+              </label>
+            </div>
+          </div>
+        )}
+
+        {totpUrl && (
+          <>
+            <div>
+              <a className="btn btn-secondary mb-2" href={totpUrl}>
+                {i18n.t("two_factor_link")}
+              </a>
+            </div>
+            <div className="form-group">
+              <div className="form-check">
+                <input
+                  className="form-check-input"
+                  id="user-remove-totp"
+                  type="checkbox"
+                  checked={
+                    this.state.saveUserSettingsForm.generate_totp_2fa == false
+                  }
+                  onChange={linkEvent(this, this.handleRemoveTotp)}
+                />
+                <label className="form-check-label" htmlFor="user-remove-totp">
+                  {i18n.t("remove_two_factor")}
+                </label>
+              </div>
+            </div>
+          </>
+        )}
+      </>
+    );
+  }
+
   setupBlockPersonChoices() {
     if (isBrowser()) {
       let selectId: any = document.getElementById("block-person-filter");
@@ -1017,6 +1071,23 @@ export class Settings extends Component<any, SettingsState> {
     i.setState(i.state);
   }
 
+  handleGenerateTotp(i: Settings, event: any) {
+    // Coerce false to undefined here, so it won't generate it.
+    let checked: boolean | undefined = event.target.checked || undefined;
+    if (checked) {
+      toast(i18n.t("two_factor_setup_instructions"));
+    }
+    i.state.saveUserSettingsForm.generate_totp_2fa = checked;
+    i.setState(i.state);
+  }
+
+  handleRemoveTotp(i: Settings, event: any) {
+    // Coerce true to undefined here, so it won't generate it.
+    let checked: boolean | undefined = !event.target.checked && undefined;
+    i.state.saveUserSettingsForm.generate_totp_2fa = checked;
+    i.setState(i.state);
+  }
+
   handleSendNotificationsToEmailChange(i: Settings, event: any) {
     i.state.saveUserSettingsForm.send_notifications_to_email =
       event.target.checked;
index 09da9dbab73dc08960845d62888cc253e8138536..bfbd5386e034d33b9fc015510a744df3e165814f 100644 (file)
@@ -500,6 +500,7 @@ export function toast(text: string, background = "success") {
       backgroundColor: backgroundColor,
       gravity: "bottom",
       position: "left",
+      duration: 5000,
     }).showToast();
   }
 }
index b9a50db7027f9804b9cdc7faf93b299a19f06e88..cf26c24b5a2ef1dc89756d62fca6f92e85d3ee5b 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -5403,10 +5403,10 @@ leac@^0.6.0:
   resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
   integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
 
-lemmy-js-client@0.17.2-rc.1:
-  version "0.17.2-rc.1"
-  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.1.tgz#fe8d1508311bbf245acc98c2c3e47e2165a95b14"
-  integrity sha512-YrOXuCofgkqp28krmPTQZAfUWL5zEDA0sRJ0abKcgf/I8YYkYkUkPS9TOORN5Lv3bc8RAAz4+2/zLHqYL/Tnow==
+lemmy-js-client@0.17.2-rc.3:
+  version "0.17.2-rc.3"
+  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.3.tgz#dc2a33e9228aef260b03a6e1f55698a2f975f979"
+  integrity sha512-FlWEPMrW2Q/FbtihLOHq2YtcRuoX7700LweCnsm6R6dD6SzsnWy9nKJhn24fcjcR2o6tw0oZKgP0ccq9jPDgfQ==
   dependencies:
     node-fetch "2.6.6"