From 7fe860ba3a309b551f227a124ff13d845cd2240b Mon Sep 17 00:00:00 2001 From: Gerben Jongerius Date: Thu, 23 May 2024 14:14:48 +0200 Subject: [PATCH] Add the 2factor dialog after login (#21) * Update the pipeline set-java module. * Fix authentication flow in the application and add the 2factor dialog. --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ------------ .github/ISSUE_TEMPLATE/feature_request.md | 20 ------ .github/workflows/default.yml | 2 +- .github/workflows/release-build.yml | 2 +- package.json | 2 +- src/App.tsx | 64 +++++++++++++++++--- src/core/Notification.tsx | 4 +- src/core/repositories/rest-api.ts | 5 +- src/core/repositories/security-repository.js | 18 +++--- src/core/sidebar/index.tsx | 7 +-- src/core/sidebar/mobile-sidebar.tsx | 7 +-- src/security/login-card.tsx | 3 - src/security/two-factor.card.tsx | 39 ++++++++++++ 13 files changed, 111 insertions(+), 100 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 src/security/two-factor.card.tsx diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 2525f1b..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: gjong - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 7e2dc1f..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: gjong - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index e633977..37135a0 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Configure Java version - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 464590e..1fc10f6 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Configure Java version - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' diff --git a/package.json b/package.json index 560c6a1..4369d31 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pledger-ui", "version": "1.3.0", "private": true, - "proxy": "http://localhost:8080/", + "proxy": "http://127.0.0.1:8080/", "homepage": "/ui", "dependencies": { "@mdi/js": "^7.4.47", diff --git a/src/App.tsx b/src/App.tsx index f045c5b..ccc07a3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,20 +12,21 @@ import { RulesRoutes } from "./rules"; import './assets/css/Main.scss' import './assets/css/Theme.scss' -import { lazy, Suspense, useState } from "react"; +import { lazy, Suspense, useEffect, useState } from "react"; import { ContractRoutes } from "./contract"; import { BudgetRoutes } from "./budget"; import { ProfileRoutes } from "./profile"; import MobileSidebar from "./core/sidebar/mobile-sidebar"; import { BatchRoutes } from "./batch"; +import TwoFactorCard from "./security/two-factor.card"; +import SecurityRepository from "./core/repositories/security-repository"; +import RestAPI from "./core/repositories/rest-api"; +import { AxiosError } from "axios"; const LoginCard = lazy(() => import("./security/login-card")); const RegisterCard = lazy(() => import("./security/RegisterCard")); -const routes = [ - } key='index' /> -] -routes.push(...AccountRoutes) +const routes = [...AccountRoutes] routes.push(...CategoryRoutes) routes.push(...SettingRoutes) routes.push(...ReportRoutes) @@ -38,18 +39,61 @@ routes.push(...ProfileRoutes) routes.push(...BatchRoutes) function App() { - const [_, setAuthenticate] = useState(false) //eslint-disable-line + const [isAuthenticated, setAuthenticate] = useState(false) //eslint-disable-line + const [twoFactorNeeded, setTwoFactor] = useState(false) //eslint-disable-line - if (sessionStorage.getItem('token')) { + const authenticated = () => { + RestAPI.profile() + .then(() => { + console.log('Profile loaded') + setAuthenticate(true) + setTwoFactor(false) + }) + .catch((ex: AxiosError) => { + if (ex.response?.status === 403) { + setTwoFactor(true) + } + }) + } + const logout = () => { + SecurityRepository.logout() + setAuthenticate(false) + } + + useEffect(() => { + console.log('App mounted') + if (sessionStorage.getItem('token')) { + authenticated() + } + + window.addEventListener('credentials-expired', logout) + }, []) + + if (twoFactorNeeded) { + return ( + + + + + }/> + }/> + + + + ) + } + + if (isAuthenticated) { return ( - setAuthenticate(false)}/> - setAuthenticate(false)}/> + +
{routes} + }/> @@ -65,7 +109,7 @@ function App() { }/> - setAuthenticate(true)} />}/> + }/> }/> } /> diff --git a/src/core/Notification.tsx b/src/core/Notification.tsx index 7d792fc..f7d435c 100644 --- a/src/core/Notification.tsx +++ b/src/core/Notification.tsx @@ -21,10 +21,10 @@ const NotificationService = (() => { } const handleException = (error: AxiosError) => { const apiError: ApiError = error.response?.data as ApiError - if (apiError._links.help) { + if (apiError?._links.help) { push(apiError._links.help[0].href, 'warning') } else { - console.error('Error intercepted', error) + notifyUser({ type: 'warning', message: (error.response as any).data.message }) } } diff --git a/src/core/repositories/rest-api.ts b/src/core/repositories/rest-api.ts index ec086a1..6551929 100644 --- a/src/core/repositories/rest-api.ts +++ b/src/core/repositories/rest-api.ts @@ -48,7 +48,8 @@ const RestAPI = (() => { return profile } - const handle = (response: Promise) => response.then(response => response.data) + const handle = (response: Promise) => response + .then(response => response.data) const api = { profile: () => api.get('profile').then(updateProfile), @@ -61,8 +62,6 @@ const RestAPI = (() => { delete: (uri: string, settings = {}): Promise => handle(axiosInstance.delete(uri, settings)) } - if (sessionStorage.getItem('token')) api.profile().finally(() => {}) - return api })() diff --git a/src/core/repositories/security-repository.js b/src/core/repositories/security-repository.js index fd0e3bb..0dbaf44 100644 --- a/src/core/repositories/security-repository.js +++ b/src/core/repositories/security-repository.js @@ -16,18 +16,18 @@ const SecurityRepository = (api => { sessionStorage.setItem('refresh-token', token.refreshToken); sessionStorage.setItem('token', token.accessToken); }), + twoFactor: (code) => api.post('security/2-factor', { verificationCode: code }) + .then(serverResponse => { + const token = new TokenResponse(serverResponse) + sessionStorage.setItem('refresh-token', token.refreshToken); + sessionStorage.setItem('token', token.accessToken); + }), register: (username, password) => api.put(`security/create-account`, { username, password }), logout: () => { - sessionStorage.removeItem('token'); - sessionStorage.removeItem('refresh-token'); - } + sessionStorage.removeItem('token'); + sessionStorage.removeItem('refresh-token'); + } } })(RestAPI) -window.addEventListener('credentials-expired', _ => { - console.log('Credentials expired') - SecurityRepository.logout() - window.location.reload() -}) - export default SecurityRepository \ No newline at end of file diff --git a/src/core/sidebar/index.tsx b/src/core/sidebar/index.tsx index 58fc099..57b43b8 100644 --- a/src/core/sidebar/index.tsx +++ b/src/core/sidebar/index.tsx @@ -16,11 +16,6 @@ type SidebarProps = { } const Sidebar = ({ logoutCallback } : SidebarProps) => { - const onLogout = () => { - dispatchEvent(new CustomEvent('credentials-expired')) - logoutCallback() - } - return <>
diff --git a/src/core/sidebar/mobile-sidebar.tsx b/src/core/sidebar/mobile-sidebar.tsx index 0076b30..27da6c0 100644 --- a/src/core/sidebar/mobile-sidebar.tsx +++ b/src/core/sidebar/mobile-sidebar.tsx @@ -8,7 +8,6 @@ import { NavLink } from "react-router-dom"; import ProfilePicture from "../../profile/profile-picture.component"; import { Buttons } from "../index"; import { mdiCloseBox, mdiLogoutVariant } from "@mdi/js"; -import SecurityRepository from "../repositories/security-repository"; type SidebarProps = { @@ -17,10 +16,6 @@ type SidebarProps = { const MobileSidebar = ({ logoutCallback } : SidebarProps) => { const [isOpen, setIsOpen] = useState(false) - const onLogout = () => { - SecurityRepository.logout() - logoutCallback() - } useEffect(() => { const onMenuClick = () => setIsOpen(previous => !previous) @@ -61,7 +56,7 @@ const MobileSidebar = ({ logoutCallback } : SidebarProps) => { className='border-none !text-white' /> diff --git a/src/security/login-card.tsx b/src/security/login-card.tsx index d729365..f9ae3b0 100644 --- a/src/security/login-card.tsx +++ b/src/security/login-card.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; import { mdiAccountPlus, mdiLogin, mdiWeb } from "@mdi/js"; import { Form, Input, SubmitButton } from '../core/form' @@ -16,11 +15,9 @@ type LoginCallback = () => void const LoginCard = ({ callback }: { callback: LoginCallback }) => { const [failure, setFailure] = useState() - const navigate = useNavigate() const onSubmit = (entity: LoginForm) => SecurityRepository.authenticate(entity.username, entity.password) .then(() => callback()) - .then(() => navigate('/dashboard')) .catch(setFailure) return ( diff --git a/src/security/two-factor.card.tsx b/src/security/two-factor.card.tsx new file mode 100644 index 0000000..853000c --- /dev/null +++ b/src/security/two-factor.card.tsx @@ -0,0 +1,39 @@ +import { Form, Input, SubmitButton } from "../core/form"; +import { Layout, Message, Notifications } from "../core"; +import { mdiCheck } from "@mdi/js"; +import SecurityRepository from "../core/repositories/security-repository"; +import { AxiosError } from "axios"; + +const TwoFactorCard = ({ callback }: { callback: () => void }) => { + const onSubmit = (entity: any) => { + SecurityRepository.twoFactor(entity.code) + .then(callback) + .catch((error: AxiosError) => Notifications.Service.exception(error)) + } + + return <> +
+
+ + ]} + className='min-w-[30rem]'> + + + + + + +
+
+ +} + +export default TwoFactorCard; \ No newline at end of file