Skip to content

Add the 2factor dialog after login #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 0 additions & 38 deletions .github/ISSUE_TEMPLATE/bug_report.md

This file was deleted.

20 changes: 0 additions & 20 deletions .github/ISSUE_TEMPLATE/feature_request.md

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
64 changes: 54 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
<Route path='/' element={<Navigate to='/dashboard'/>} key='index' />
]
routes.push(...AccountRoutes)
const routes = [...AccountRoutes]
routes.push(...CategoryRoutes)
routes.push(...SettingRoutes)
routes.push(...ReportRoutes)
Expand All @@ -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 (
<Suspense>
<BrowserRouter basename='/ui'>
<Notifications.NotificationCenter />
<Routes>
<Route key='two-factor' path="/two-factor" element={<TwoFactorCard callback={ authenticated }/>}/>
<Route key='redirect' path='/*' element={<Navigate to='/two-factor'/>}/>
</Routes>
</BrowserRouter>
</Suspense>
)
}

if (isAuthenticated) {
return (
<Suspense>
<BrowserRouter basename='/ui'>
<Sidebar logoutCallback={() => setAuthenticate(false)}/>
<MobileSidebar logoutCallback={() => setAuthenticate(false)}/>
<Sidebar logoutCallback={ logout }/>
<MobileSidebar logoutCallback={ logout }/>
<main className='Main px-2 md:px-5 h-[100vh] flex flex-col overflow-y-auto'>
<Notifications.NotificationCenter />
<Routes>
{routes}
<Route path='/*' element={<Navigate to='/dashboard'/>}/>
</Routes>
<Suspense>
<Outlet />
Expand All @@ -65,7 +109,7 @@ function App() {
<BrowserRouter basename='/ui'>
<Routes>
<Route path='/' element={<Navigate to='/login'/>}/>
<Route path='/login' element={<LoginCard callback={() => setAuthenticate(true)} />}/>
<Route path='/login' element={<LoginCard callback={ authenticated } />}/>
<Route path='/register' element={<RegisterCard />}/>
<Route path='/*' element={<Navigate to='/login'/>} />
</Routes>
Expand Down
4 changes: 2 additions & 2 deletions src/core/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}

Expand Down
5 changes: 2 additions & 3 deletions src/core/repositories/rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const RestAPI = (() => {
return profile
}

const handle = (response: Promise<AxiosResponse>) => response.then(response => response.data)
const handle = (response: Promise<AxiosResponse>) => response
.then(response => response.data)

const api = {
profile: () => api.get('profile').then(updateProfile),
Expand All @@ -61,8 +62,6 @@ const RestAPI = (() => {
delete: (uri: string, settings = {}): Promise<void> => handle(axiosInstance.delete(uri, settings))
}

if (sessionStorage.getItem('token')) api.profile().finally(() => {})

return api
})()

Expand Down
18 changes: 9 additions & 9 deletions src/core/repositories/security-repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 1 addition & 6 deletions src/core/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ type SidebarProps = {
}

const Sidebar = ({ logoutCallback } : SidebarProps) => {
const onLogout = () => {
dispatchEvent(new CustomEvent('credentials-expired'))
logoutCallback()
}

return <>
<div className='h-screen max-w-[218px] flex-col overflow-y-auto
hidden md:flex
Expand Down Expand Up @@ -55,7 +50,7 @@ const Sidebar = ({ logoutCallback } : SidebarProps) => {
</NavLink>

<Buttons.Button icon={ mdiLogoutVariant }
onClick={ onLogout }
onClick={ logoutCallback }
variant='icon'
className='px-2 text-[var(--sidebar-icon-color)]'/>
</footer>
Expand Down
7 changes: 1 addition & 6 deletions src/core/sidebar/mobile-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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)
Expand Down Expand Up @@ -61,7 +56,7 @@ const MobileSidebar = ({ logoutCallback } : SidebarProps) => {
className='border-none !text-white' />

<Buttons.Button icon={ mdiLogoutVariant }
onClick={ onLogout }
onClick={ logoutCallback }
variant='icon'
className='px-2 text-[var(--sidebar-icon-color)]'/>
</footer>
Expand Down
3 changes: 0 additions & 3 deletions src/security/login-card.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 (
Expand Down
39 changes: 39 additions & 0 deletions src/security/two-factor.card.tsx
Original file line number Diff line number Diff line change
@@ -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 <>
<div className='flex justify-center h-[100vh] items-center'>
<Form entity='UserAccount' onSubmit={ onSubmit }>
<Layout.Card title='page.login.verify.title'
buttons={[
<SubmitButton key='verify'
label='page.login.verify.action'
icon={ mdiCheck }/>
]}
className='min-w-[30rem]'>

<Message label='page.login.verify.explain' variant='info' />

<Input.Text id='code'
title='UserAccount.twofactor.secret'
type='text'
pattern="^[0-9]{6}$"
required />

</Layout.Card>
</Form>
</div>
</>
}

export default TwoFactorCard;
Loading