From e414ad3849cdae0549f228197ac3ec9e52138378 Mon Sep 17 00:00:00 2001
From: Tin Geber <tin@greenhost.nl>
Date: Thu, 12 Oct 2023 10:55:18 +0200
Subject: [PATCH] reset 2FA working

---
 frontend/public/index.html                    |  22 +---
 frontend/src/components/Modal/Modal/Modal.tsx |   2 +-
 .../src/components/UserModal/UserModal.tsx    | 106 +++++++++++++-----
 .../src/services/users/hooks/use-users.ts     |   6 +
 frontend/src/services/users/redux/actions.ts  |  18 +++
 .../src/services/users/transformations.ts     |   8 ++
 6 files changed, 110 insertions(+), 52 deletions(-)

diff --git a/frontend/public/index.html b/frontend/public/index.html
index 76284b1b..6d6ba053 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -8,36 +8,16 @@
   <meta name="theme-color" content="#000000" />
   <meta name="description" content="Stackspin" />
   <link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
-  <!--
-      manifest.json provides metadata used when your web app is installed on a
-      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-    -->
+
   <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
-  <!--
-      Notice the use of %PUBLIC_URL% in the tags above.
-      It will be replaced with the URL of the `public` folder during the build.
-      Only files inside the `public` folder can be referenced from the HTML.
 
-      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
-      work correctly both with client-side routing and a non-root public URL.
-      Learn how to configure a non-root public URL by running `npm run build`.
-    -->
   <title>Stackspin Dashboard</title>
 </head>
 
 <body>
   <noscript>You need to enable JavaScript to run this app.</noscript>
   <div id="root"></div>
-  <!--
-      This HTML file is a template.
-      If you open it directly in the browser, you will see an empty page.
-
-      You can add webfonts, meta tags, or analytics to this file.
-      The build step will place the bundled scripts into the <body> tag.
 
-      To begin the development, run `npm start` or `yarn start`.
-      To create a production bundle, use `npm run build` or `yarn build`.
-    -->
 </body>
 
 </html>
\ No newline at end of file
diff --git a/frontend/src/components/Modal/Modal/Modal.tsx b/frontend/src/components/Modal/Modal/Modal.tsx
index 24bd4813..c5a0694f 100644
--- a/frontend/src/components/Modal/Modal/Modal.tsx
+++ b/frontend/src/components/Modal/Modal/Modal.tsx
@@ -88,7 +88,7 @@ export const Modal: React.FC<ModalProps> = ({
                 <div className="bg-white px-4 p-6 relative overflow-y-auto">{children}</div>
 
                 {onSave && (
-                  <div className="bg-gray-50 px-4 py-5 flex flex-shrink-0 w-full">
+                  <div className="bg-gray-50 px-4 py-5 flex flex-shrink-0 w-full border-0 border-t-2 border-gray-300">
                     <div className="mr-auto sm:flex gap-2">{leftActions}</div>
                     <div className="ml-auto sm:flex sm:flex-row-reverse">
                       <button
diff --git a/frontend/src/components/UserModal/UserModal.tsx b/frontend/src/components/UserModal/UserModal.tsx
index 838c955b..7e089fdd 100644
--- a/frontend/src/components/UserModal/UserModal.tsx
+++ b/frontend/src/components/UserModal/UserModal.tsx
@@ -1,6 +1,6 @@
 import React, { useEffect, useState } from 'react';
 import _ from 'lodash';
-import { TrashIcon } from '@heroicons/react/outline';
+import { TrashIcon, KeyIcon, QrcodeIcon } from '@heroicons/react/outline';
 import { useFieldArray, useForm, useWatch } from 'react-hook-form';
 import { Banner, Modal, ConfirmationModal, InfoModal } from 'src/components';
 import { Input, Select } from 'src/components/Form';
@@ -16,6 +16,7 @@ import { UserModalProps } from './types';
 export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalProps) => {
   const [deleteModal, setDeleteModal] = useState(false);
   const [passwordLinkModal, setPasswordLinkModal] = useState(false);
+  const [totpModal, setTotpModal] = useState(false);
   const [isAdminRoleSelected, setAdminRoleSelected] = useState(true);
   const [isPersonalModal, setPersonalModal] = useState(false);
   const {
@@ -30,6 +31,7 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
     deleteUserById,
     getRecoveryLinkUserById,
     clearSelectedUser,
+    resetTotp,
   } = useUsers();
   const { currentUser, isAdmin } = useAuth();
 
@@ -126,18 +128,18 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
     setUserId(null);
   };
 
-  const handleKeyPress = (e: any) => {
-    if (e.key === 'Enter' || e.key === 'NumpadEnter') {
-      handleSave();
-    }
-  };
-
   const handleClose = () => {
     onClose();
     clearSelectedUser();
     setUserId(null);
   };
 
+  const handleKeyPress = (e: any) => {
+    if (e.key === 'Enter' || e.key === 'NumpadEnter') {
+      handleSave();
+    }
+  };
+
   const deleteModalOpen = () => setDeleteModal(true);
   const deleteModalClose = () => setDeleteModal(false);
 
@@ -150,6 +152,15 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
 
   const passwordLinkModalClose = () => setPasswordLinkModal(false);
 
+  const totpModalOpen = () => {
+    if (userId) {
+      resetTotp(userId);
+    }
+    setTotpModal(true);
+  };
+
+  const totpModalClose = () => setTotpModal(false);
+
   const handleDelete = () => {
     if (userId) {
       deleteUserById(userId);
@@ -171,8 +182,8 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
           type="button"
           className="mb-4 sm:mb-0 inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
         >
-          <TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
-          Delete
+          Delete user
+          <TrashIcon className="-mr-0.5 ml-2 h-4 w-4" aria-hidden="true" />
         </button>
       )
     );
@@ -187,9 +198,10 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
           onClick={passwordLinkModalOpen}
           type="button"
           className="mb-4 sm:mb-0 inline-flex items-center px-4 py-2 text-sm
-  font-medium rounded-md text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+  font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-700"
         >
           Password Link
+          <KeyIcon className="-mr-0.5 ml-2 h-4 w-4" aria-hidden="true" />
         </button>
       )
     );
@@ -202,12 +214,13 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
       isAdmin &&
       user.totp && (
         <button
-          onClick={passwordLinkModalOpen}
+          onClick={totpModalOpen}
           type="button"
           className="mb-4 sm:mb-0 inline-flex items-center px-4 py-2 text-sm
-  font-medium rounded-md text-yellow-400 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+  font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-700"
         >
           Reset 2FA
+          <QrcodeIcon className="-mr-0.5 ml-2 h-4 w-4" aria-hidden="true" />
         </button>
       )
     );
@@ -215,26 +228,14 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
 
   return (
     <>
-      <Modal
-        onClose={handleClose}
-        open={open}
-        onSave={handleSave}
-        isLoading={userModalLoading}
-        leftActions={
-          <>
-            {buttonDelete()}
-            {buttonPasswordLink()}
-            {buttonTotp()}
-          </>
-        }
-        useCancelButton
-      >
+      <Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={userModalLoading} useCancelButton>
         <div className="bg-white px-4">
           <div className="space-y-10 divide-y divide-gray-200">
             <div>
-              <div>
-                <h3 className="text-lg leading-6 font-medium text-gray-900">{userId ? 'Edit user' : 'Add new user'}</h3>
-                {user.totp && <div>hello</div>}
+              <div className="py-4">
+                <h3 className="text-lg leading-6 font-medium text-gray-900">
+                  {userId ? <span>Edit {user.email}</span> : 'Add new user'}
+                </h3>
               </div>
 
               <div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
@@ -302,7 +303,7 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
                 </div>
 
                 {isAdminRoleSelected && (
-                  <div className="sm:col-span-6">
+                  <div className="sm:col-span-6 py-12">
                     <Banner
                       title="Admin users automatically have admin-level access to all apps."
                       titleSm="Admin user"
@@ -355,6 +356,44 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
                 )}
               </div>
             )}
+            {isAdmin && !userModalLoading && (
+              <div>
+                <div className="mt-8">
+                  <h3 className="text-lg leading-6 font-medium text-gray-900">User Access</h3>
+                </div>
+                <div>
+                  <div className="flow-root mt-6">
+                    <ul className="-my-5 divide-y divide-gray-200">
+                      <li className="py-4">
+                        <div className="flex items-center justify-between">
+                          <p className="leading-6 text-sm  text-gray-500">
+                            Generate password reset link for {user.email}
+                          </p>
+                          {buttonPasswordLink()}
+                        </div>
+                      </li>
+                      {user.totp && (
+                        <li className="py-4">
+                          <div className="flex items-center justify-between">
+                            <p className="leading-6 text-sm  text-gray-500">Reset 2-factor authentication</p>
+                            {buttonTotp()}
+                          </div>
+                        </li>
+                      )}
+
+                      {user.email !== currentUser?.email && (
+                        <li className="py-4">
+                          <div className="flex items-center justify-between">
+                            <p className="leading-6 text-sm  text-gray-500">Delete {user.email}</p>
+                            {buttonDelete()}
+                          </div>
+                        </li>
+                      )}
+                    </ul>
+                  </div>
+                </div>
+              </div>
+            )}
           </div>
         </div>
       </Modal>
@@ -372,6 +411,13 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP
         body="Below is a password reset link. Copy this link and send to the user to reset the password without using e-mail"
         dynamicData={recoveryLink}
       />
+      <InfoModal
+        open={totpModal}
+        onClose={totpModalClose}
+        title="Reset 2-Factor Authentication"
+        body="You have successfully removed the user's 2FA device."
+        dynamicData={recoveryLink}
+      />
     </>
   );
 };
diff --git a/frontend/src/services/users/hooks/use-users.ts b/frontend/src/services/users/hooks/use-users.ts
index cf189fc8..ebcbb917 100644
--- a/frontend/src/services/users/hooks/use-users.ts
+++ b/frontend/src/services/users/hooks/use-users.ts
@@ -12,6 +12,7 @@ import {
   clearCurrentUser,
   createBatchUsers,
   fetchRecoveryLink,
+  resetTotpById,
 } from '../redux';
 import { getUserById, getRecoveryLink, getUserModalLoading, getUserslLoading } from '../redux/selectors';
 
@@ -66,6 +67,10 @@ export function useUsers() {
     return dispatch(fetchRecoveryLink(id));
   }
 
+  function resetTotp(id: string) {
+    return dispatch(resetTotpById(id));
+  }
+
   return {
     users,
     user,
@@ -83,5 +88,6 @@ export function useUsers() {
     getRecoveryLinkUserById,
     clearSelectedUser,
     createUsers,
+    resetTotp,
   };
 }
diff --git a/frontend/src/services/users/redux/actions.ts b/frontend/src/services/users/redux/actions.ts
index 394a9340..76e7eaad 100644
--- a/frontend/src/services/users/redux/actions.ts
+++ b/frontend/src/services/users/redux/actions.ts
@@ -12,6 +12,7 @@ import {
   transformUser,
   transformUpdateMultipleUsers,
   transformRecoveryLink,
+  transformTotp,
 } from '../transformations';
 
 export enum UserActionTypes {
@@ -25,6 +26,7 @@ export enum UserActionTypes {
   SET_USERS_LOADING = 'users/users_loading',
   CREATE_BATCH_USERS = 'users/create_batch_users',
   UPDATE_MULTIPLE_USERS = '/users/multi-edit',
+  RESET_TOTP = 'users/reset-totp-user',
 }
 
 export const setUsersLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
@@ -235,6 +237,22 @@ export const fetchRecoveryLink = (id: string) => async (dispatch: Dispatch<any>)
   }
 };
 
+export const resetTotpById = (id: string) => async (dispatch: Dispatch<any>) => {
+  try {
+    const { data } = await performApiCall({
+      path: `/users/${id}/reset_2fa`,
+      method: 'POST',
+    });
+
+    dispatch({
+      type: UserActionTypes.RESET_TOTP,
+      payload: transformTotp(data),
+    });
+  } catch (err) {
+    console.error(err);
+  }
+};
+
 export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => {
   dispatch(setUserModalLoading(true));
 
diff --git a/frontend/src/services/users/transformations.ts b/frontend/src/services/users/transformations.ts
index 41e1525b..e430c1f0 100644
--- a/frontend/src/services/users/transformations.ts
+++ b/frontend/src/services/users/transformations.ts
@@ -119,3 +119,11 @@ export const transformBatchResponse = (response: any): any => {
 export const transformRecoveryLink = (response: any): string => {
   return response.recovery_link;
 };
+
+export const transformResetTotpById = (response: any): any => {
+  return {
+    success: response.success,
+    existing: response.existing,
+    failed: response.failed,
+  };
+};
-- 
GitLab