feat: add authenticated routing to pazuz project

This commit is contained in:
Sérgio Salgado 2021-05-12 19:40:11 +01:00 committed by Josh Harvey
parent 047b5752b7
commit 019872ff31
3 changed files with 172 additions and 66 deletions

View file

@ -1,15 +1,17 @@
import { useQuery } from '@apollo/react-hooks'
import CssBaseline from '@material-ui/core/CssBaseline' import CssBaseline from '@material-ui/core/CssBaseline'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import Slide from '@material-ui/core/Slide'
import { import {
StylesProvider, StylesProvider,
jssPreset, jssPreset,
MuiThemeProvider, MuiThemeProvider,
makeStyles makeStyles
} from '@material-ui/core/styles' } from '@material-ui/core/styles'
import { axios } from '@use-hooks/axios' import gql from 'graphql-tag'
import { create } from 'jss' import { create } from 'jss'
import extendJss from 'jss-plugin-extend' import extendJss from 'jss-plugin-extend'
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useState } from 'react'
import { import {
useLocation, useLocation,
useHistory, useHistory,
@ -69,11 +71,32 @@ const useStyles = makeStyles({
} }
}) })
const GET_USER_DATA = gql`
query userData {
userData {
id
username
role
enabled
last_accessed
last_accessed_from
last_accessed_address
}
}
`
const Main = () => { const Main = () => {
const classes = useStyles() const classes = useStyles()
const location = useLocation() const location = useLocation()
const history = useHistory() const history = useHistory()
const { wizardTested, userData } = useContext(AppContext) const { wizardTested, userData, setUserData } = useContext(AppContext)
const { loading } = useQuery(GET_USER_DATA, {
onCompleted: userResponse => {
if (!userData && userResponse?.userData)
setUserData(userResponse.userData)
}
})
const route = location.pathname const route = location.pathname
@ -97,7 +120,17 @@ const Main = () => {
)} )}
<main className={classes.wrapper}> <main className={classes.wrapper}>
{sidebar && !is404 && wizardTested && ( {sidebar && !is404 && wizardTested && (
<TitleSection title={parent.title}></TitleSection> <Slide
direction="left"
in={true}
mountOnEnter
unmountOnExit
children={
<div>
<TitleSection title={parent.title}></TitleSection>
</div>
}
/>
)} )}
<Grid container className={classes.grid}> <Grid container className={classes.grid}>
@ -109,9 +142,7 @@ const Main = () => {
onClick={onClick} onClick={onClick}
/> />
)} )}
<div className={contentClassName}> <div className={contentClassName}>{!loading && <Routes />}</div>
<Routes />
</div>
</Grid> </Grid>
</main> </main>
</div> </div>
@ -121,46 +152,26 @@ const Main = () => {
const App = () => { const App = () => {
const [wizardTested, setWizardTested] = useState(false) const [wizardTested, setWizardTested] = useState(false)
const [userData, setUserData] = useState(null) const [userData, setUserData] = useState(null)
const [loading, setLoading] = useState(true)
const url = const setRole = role => {
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' if (userData && userData.role !== role) {
setUserData({ ...userData, role })
useEffect(() => { }
getUserData()
}, [])
const getUserData = () => {
axios({
method: 'GET',
url: `${url}/user-data`,
withCredentials: true
})
.then(res => {
setLoading(false)
if (res.status === 200) setUserData(res.data.user)
})
.catch(err => {
setLoading(false)
if (err.status === 403) setUserData(null)
})
} }
return ( return (
<AppContext.Provider <AppContext.Provider
value={{ wizardTested, setWizardTested, userData, setUserData }}> value={{ wizardTested, setWizardTested, userData, setUserData, setRole }}>
{!loading && ( <Router>
<Router> <ApolloProvider>
<ApolloProvider> <StylesProvider jss={jss}>
<StylesProvider jss={jss}> <MuiThemeProvider theme={theme}>
<MuiThemeProvider theme={theme}> <CssBaseline />
<CssBaseline /> <Main />
<Main /> </MuiThemeProvider>
</MuiThemeProvider> </StylesProvider>
</StylesProvider> </ApolloProvider>
</ApolloProvider> </Router>
</Router>
)}
</AppContext.Provider> </AppContext.Provider>
) )
} }

View file

@ -4,20 +4,23 @@ import { ApolloClient } from 'apollo-client'
import { ApolloLink } from 'apollo-link' import { ApolloLink } from 'apollo-link'
import { onError } from 'apollo-link-error' import { onError } from 'apollo-link-error'
import { HttpLink } from 'apollo-link-http' import { HttpLink } from 'apollo-link-http'
import React from 'react' import React, { useContext } from 'react'
import { useHistory, useLocation } from 'react-router-dom' import { useHistory, useLocation } from 'react-router-dom'
import AppContext from 'src/AppContext'
const URI = const URI =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
const getClient = (history, location) => const getClient = (history, location, setUserData, setRole) =>
new ApolloClient({ new ApolloClient({
link: ApolloLink.from([ link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => { onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path, extensions }) => { graphQLErrors.forEach(({ message, locations, path, extensions }) => {
if (extensions?.code === 'UNAUTHENTICATED') { if (extensions?.code === 'UNAUTHENTICATED') {
if (location.pathname !== '/404') history.push('/404') setUserData(null)
if (location.pathname !== '/login') history.push('/login')
} }
console.log( console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
@ -25,6 +28,21 @@ const getClient = (history, location) =>
}) })
if (networkError) console.log(`[Network error]: ${networkError}`) if (networkError) console.log(`[Network error]: ${networkError}`)
}), }),
new ApolloLink((operation, forward) => {
return forward(operation).map(response => {
const context = operation.getContext()
const {
response: { headers }
} = context
if (headers) {
const role = headers.get('role')
setRole(role)
}
return response
})
}),
new HttpLink({ new HttpLink({
credentials: 'include', credentials: 'include',
uri: `${URI}/graphql` uri: `${URI}/graphql`
@ -49,7 +67,9 @@ const getClient = (history, location) =>
const Provider = ({ children }) => { const Provider = ({ children }) => {
const history = useHistory() const history = useHistory()
const location = useLocation() const location = useLocation()
const client = getClient(history, location) const { setUserData, setRole } = useContext(AppContext)
const client = getClient(history, location, setUserData, setRole)
return <ApolloProvider client={client}>{children}</ApolloProvider> return <ApolloProvider client={client}>{children}</ApolloProvider>
} }

View file

@ -5,7 +5,6 @@ import * as R from 'ramda'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { import {
matchPath, matchPath,
Route,
Redirect, Redirect,
Switch, Switch,
useHistory, useHistory,
@ -13,11 +12,14 @@ import {
} from 'react-router-dom' } from 'react-router-dom'
import AppContext from 'src/AppContext' import AppContext from 'src/AppContext'
import AuthRegister from 'src/pages/AuthRegister' import Login from 'src/pages/Authentication/Login'
import Register from 'src/pages/Authentication/Register'
import Reset2FA from 'src/pages/Authentication/Reset2FA'
import ResetPassword from 'src/pages/Authentication/ResetPassword'
import Blacklist from 'src/pages/Blacklist' import Blacklist from 'src/pages/Blacklist'
import Cashout from 'src/pages/Cashout' import Cashout from 'src/pages/Cashout'
import Commissions from 'src/pages/Commissions' import Commissions from 'src/pages/Commissions'
import ConfigMigration from 'src/pages/ConfigMigration' // import ConfigMigration from 'src/pages/ConfigMigration'
import { Customers, CustomerProfile } from 'src/pages/Customers' import { Customers, CustomerProfile } from 'src/pages/Customers'
import Dashboard from 'src/pages/Dashboard' import Dashboard from 'src/pages/Dashboard'
import Funding from 'src/pages/Funding' import Funding from 'src/pages/Funding'
@ -34,9 +36,14 @@ import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions' import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
import ServerLogs from 'src/pages/ServerLogs' import ServerLogs from 'src/pages/ServerLogs'
import Services from 'src/pages/Services/Services' import Services from 'src/pages/Services/Services'
import SessionManagement from 'src/pages/SessionManagement/SessionManagement'
import Transactions from 'src/pages/Transactions/Transactions' import Transactions from 'src/pages/Transactions/Transactions'
import Triggers from 'src/pages/Triggers' import Triggers from 'src/pages/Triggers'
import UserManagement from 'src/pages/UserManagement/UserManagement'
import Wizard from 'src/pages/Wizard' import Wizard from 'src/pages/Wizard'
import PrivateRoute from 'src/routing/PrivateRoute'
import PublicRoute from 'src/routing/PublicRoute'
import { ROLES } from 'src/routing/utils'
import { namespaces } from 'src/utils/config' import { namespaces } from 'src/utils/config'
const useStyles = makeStyles({ const useStyles = makeStyles({
@ -53,12 +60,14 @@ const tree = [
key: 'transactions', key: 'transactions',
label: 'Transactions', label: 'Transactions',
route: '/transactions', route: '/transactions',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Transactions component: Transactions
}, },
{ {
key: 'maintenance', key: 'maintenance',
label: 'Maintenance', label: 'Maintenance',
route: '/maintenance', route: '/maintenance',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
get component() { get component() {
return () => <Redirect to={this.children[0].route} /> return () => <Redirect to={this.children[0].route} />
}, },
@ -67,30 +76,35 @@ const tree = [
key: 'cash_cassettes', key: 'cash_cassettes',
label: 'Cash Cassettes', label: 'Cash Cassettes',
route: '/maintenance/cash-cassettes', route: '/maintenance/cash-cassettes',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: CashCassettes component: CashCassettes
}, },
{ {
key: 'funding', key: 'funding',
label: 'Funding', label: 'Funding',
route: '/maintenance/funding', route: '/maintenance/funding',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Funding component: Funding
}, },
{ {
key: 'logs', key: 'logs',
label: 'Machine Logs', label: 'Machine Logs',
route: '/maintenance/logs', route: '/maintenance/logs',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: MachineLogs component: MachineLogs
}, },
{ {
key: 'machine-status', key: 'machine-status',
label: 'Machine Status', label: 'Machine Status',
route: '/maintenance/machine-status', route: '/maintenance/machine-status',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: MachineStatus component: MachineStatus
}, },
{ {
key: 'server-logs', key: 'server-logs',
label: 'Server', label: 'Server',
route: '/maintenance/server-logs', route: '/maintenance/server-logs',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: ServerLogs component: ServerLogs
} }
] ]
@ -99,6 +113,7 @@ const tree = [
key: 'settings', key: 'settings',
label: 'Settings', label: 'Settings',
route: '/settings', route: '/settings',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
get component() { get component() {
return () => <Redirect to={this.children[0].route} /> return () => <Redirect to={this.children[0].route} />
}, },
@ -107,30 +122,35 @@ const tree = [
key: namespaces.COMMISSIONS, key: namespaces.COMMISSIONS,
label: 'Commissions', label: 'Commissions',
route: '/settings/commissions', route: '/settings/commissions',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Commissions component: Commissions
}, },
{ {
key: namespaces.LOCALE, key: namespaces.LOCALE,
label: 'Locales', label: 'Locales',
route: '/settings/locale', route: '/settings/locale',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Locales component: Locales
}, },
{ {
key: namespaces.CASH_OUT, key: namespaces.CASH_OUT,
label: 'Cash-out', label: 'Cash-out',
route: '/settings/cash-out', route: '/settings/cash-out',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Cashout component: Cashout
}, },
{ {
key: namespaces.NOTIFICATIONS, key: namespaces.NOTIFICATIONS,
label: 'Notifications', label: 'Notifications',
route: '/settings/notifications', route: '/settings/notifications',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Notifications component: Notifications
}, },
{ {
key: 'services', key: 'services',
label: '3rd party services', label: '3rd party services',
route: '/settings/3rd-party-services', route: '/settings/3rd-party-services',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Services component: Services
}, },
{ {
@ -138,6 +158,7 @@ const tree = [
label: 'Operator Info', label: 'Operator Info',
route: '/settings/operator-info', route: '/settings/operator-info',
title: 'Operator Information', title: 'Operator Information',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
get component() { get component() {
return () => ( return () => (
<Redirect <Redirect
@ -153,24 +174,28 @@ const tree = [
key: 'contact-info', key: 'contact-info',
label: 'Contact information', label: 'Contact information',
route: '/settings/operator-info/contact-info', route: '/settings/operator-info/contact-info',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: ContactInfo component: ContactInfo
}, },
{ {
key: 'receipt-printing', key: 'receipt-printing',
label: 'Receipt', label: 'Receipt',
route: '/settings/operator-info/receipt-printing', route: '/settings/operator-info/receipt-printing',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: ReceiptPrinting component: ReceiptPrinting
}, },
{ {
key: 'coin-atm-radar', key: 'coin-atm-radar',
label: 'Coin ATM Radar', label: 'Coin ATM Radar',
route: '/settings/operator-info/coin-atm-radar', route: '/settings/operator-info/coin-atm-radar',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: CoinAtmRadar component: CoinAtmRadar
}, },
{ {
key: 'terms-conditions', key: 'terms-conditions',
label: 'Terms & Conditions', label: 'Terms & Conditions',
route: '/settings/operator-info/terms-conditions', route: '/settings/operator-info/terms-conditions',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: TermsConditions component: TermsConditions
} }
] ]
@ -181,6 +206,7 @@ const tree = [
key: 'compliance', key: 'compliance',
label: 'Compliance', label: 'Compliance',
route: '/compliance', route: '/compliance',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
get component() { get component() {
return () => <Redirect to={this.children[0].route} /> return () => <Redirect to={this.children[0].route} />
}, },
@ -189,32 +215,62 @@ const tree = [
key: 'triggers', key: 'triggers',
label: 'Triggers', label: 'Triggers',
route: '/compliance/triggers', route: '/compliance/triggers',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Triggers component: Triggers
}, },
{ {
key: 'customers', key: 'customers',
label: 'Customers', label: 'Customers',
route: '/compliance/customers', route: '/compliance/customers',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Customers component: Customers
}, },
{ {
key: 'blacklist', key: 'blacklist',
label: 'Blacklist', label: 'Blacklist',
route: '/compliance/blacklist', route: '/compliance/blacklist',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: Blacklist component: Blacklist
}, },
{ {
key: 'promo-codes', key: 'promo-codes',
label: 'Promo Codes', label: 'Promo Codes',
route: '/compliance/loyalty/codes', route: '/compliance/loyalty/codes',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: PromoCodes component: PromoCodes
}, },
{ {
key: 'customer', key: 'customer',
route: '/compliance/customer/:id', route: '/compliance/customer/:id',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: CustomerProfile component: CustomerProfile
} }
] ]
},
{
key: 'system',
label: 'System',
route: '/system',
allowedRoles: [ROLES.SUPERUSER],
get component() {
return () => <Redirect to={this.children[0].route} />
},
children: [
{
key: 'user-management',
label: 'User Management',
route: '/system/user-management',
allowedRoles: [ROLES.SUPERUSER],
component: UserManagement
},
{
key: 'session-management',
label: 'Session Management',
route: '/system/session-management',
allowedRoles: [ROLES.SUPERUSER],
component: SessionManagement
}
]
} }
] ]
@ -252,13 +308,29 @@ const Routes = () => {
const history = useHistory() const history = useHistory()
const location = useLocation() const location = useLocation()
const { wizardTested, userData } = useContext(AppContext)
const { wizardTested } = useContext(AppContext) const dontTriggerPages = [
'/404',
const dontTriggerPages = ['/404', '/register', '/wizard'] '/register',
'/wizard',
'/login',
'/register',
'/resetpassword',
'/reset2fa'
]
if (!wizardTested && !R.contains(location.pathname)(dontTriggerPages)) { if (!wizardTested && !R.contains(location.pathname)(dontTriggerPages)) {
history.push('/wizard') history.push('/wizard')
return null
}
const getFilteredRoutes = () => {
if (!userData) return []
return flattened.filter(value => {
const keys = value.allowedRoles
return R.includes(userData.role, keys)
})
} }
const Transition = location.state ? Slide : Fade const Transition = location.state ? Slide : Fade
@ -276,10 +348,10 @@ const Routes = () => {
return ( return (
<Switch> <Switch>
<Route exact path="/"> <PrivateRoute exact path="/">
<Redirect to={{ pathname: '/dashboard' }} /> <Redirect to={{ pathname: '/dashboard' }} />
</Route> </PrivateRoute>
<Route path={'/dashboard'}> <PrivateRoute path={'/dashboard'}>
<Transition <Transition
className={classes.wrapper} className={classes.wrapper}
{...transitionProps} {...transitionProps}
@ -292,13 +364,16 @@ const Routes = () => {
</div> </div>
} }
/> />
</Route> </PrivateRoute>
<Route path="/machines" component={Machines} /> <PrivateRoute path="/machines" component={Machines} />
<Route path="/wizard" component={Wizard} /> <PrivateRoute path="/wizard" component={Wizard} />
<Route path="/register" component={AuthRegister} /> <PublicRoute path="/register" component={Register} />
<Route path="/configmigration" component={ConfigMigration} /> {/* <Route path="/configmigration" component={ConfigMigration} /> */}
{flattened.map(({ route, component: Page, key }) => ( <PublicRoute path="/login" restricted component={Login} />
<Route path={route} key={key}> <PublicRoute path="/resetpassword" component={ResetPassword} />
<PublicRoute path="/reset2fa" component={Reset2FA} />
{getFilteredRoutes().map(({ route, component: Page, key }) => (
<PrivateRoute path={route} key={key}>
<Transition <Transition
className={classes.wrapper} className={classes.wrapper}
{...transitionProps} {...transitionProps}
@ -311,12 +386,12 @@ const Routes = () => {
</div> </div>
} }
/> />
</Route> </PrivateRoute>
))} ))}
<Route path="/404" /> <PublicRoute path="/404" />
<Route path="*"> <PublicRoute path="*">
<Redirect to={{ pathname: '/404' }} /> <Redirect to={{ pathname: '/404' }} />
</Route> </PublicRoute>
</Switch> </Switch>
) )
} }