Skip to content
Snippets Groups Projects
Commit 159ea62c authored by Defendi Alberto's avatar Defendi Alberto
Browse files

Merge branch 'dev' into 'master'

Refined auth flow and new website pages.

See merge request !56
parents a633ab44 92e6645f
No related branches found
No related tags found
2 merge requests!56Refined auth flow and new website pages.,!40Sign up/Log in form nest into AuthUser
Pipeline #12496 passed
Showing
with 294 additions and 96 deletions
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
The folders are divided basing on if the user is authenticated or not authenticated.
For this, the pages are organized as:
- AuthUser: private pages (for authenticated users).
Those pages must be covered by a PrivateRoute except for SignInForm and SignUpForm.
- NonAuthUser: public pages, require a normal Route.
\ No newline at end of file
import { createMuiTheme } from '@material-ui/core/styles';
export const muiTheme = createMuiTheme({
palette: {
primary: {
main: '#5e5ce4',
},
secondary: {
main: '#e2e45c',
},
},
typography: {
fontSize: 16,
},
overrides: {
MuiTabs: {
indicator: {
backgroundColor: 'white',
},
},
MuiTab: {
wrapper: {
flexDirection: 'row',
},
},
},
});
import React, { FC } from 'react'; import React, { FC } from 'react';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import { ThemeProvider } from '@material-ui/core/styles';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { HomePage } from 'components/HomePage/HomePage'; import { AuthRoutes, NonAuthRoutes } from 'api/routes';
import { AuthUser } from 'components/AuthUser/AuthUser'; import { Roles } from 'api/userRoles';
import { LandingPage } from 'components/LandingPage/LandingPage'; import { configDjangoCookieName } from 'api/configDjangoCookieName';
import { PrivateRoute } from 'components/api/PrivateRoute/PrivateRoute'; import { muiTheme } from 'App.style';
import { AuthRoutes, NonAuthRoutes } from 'components/api/routes'; import { withAuthorization } from 'components/Authorization/Authorization';
import { useRole } from 'hooks/useRole';
import { useAuth } from 'hooks/useAuth';
import { AuthUser } from 'components/Auth/Auth';
import { AuthContext } from 'components/Auth/AuthContext';
import { Dashboard } from 'components/Dashboard/Dashboard';
import { NotFound } from 'components/NotFound/NotFound'; import { NotFound } from 'components/NotFound/NotFound';
import { ProfilePage } from 'components/ProfilePage/ProfilePage';
import { Roles } from 'components/api/userRoles';
import { Unauthorized } from 'components/Unauthorized/Unauthorized'; import { Unauthorized } from 'components/Unauthorized/Unauthorized';
import { LandingPage } from 'components/LandingPage/LandingPage';
const theme = createMuiTheme({ const All = withAuthorization([
palette: { Roles.admin,
primary: { Roles.operator,
main: '#5e5ce4', Roles.senior,
}, Roles.driver,
secondary: { ]);
main: '#e2e45c',
},
},
typography: { /**
fontSize: 16, * Entry point of the app.
}, */
export const App: FC = () => {
configDjangoCookieName();
const [role, setRole] = useRole();
const [isAuth, setIsAuth] = useAuth();
overrides: { const value = { role, setRole, isAuth, setIsAuth };
MuiTabs: {
indicator: {
backgroundColor: 'white',
},
},
MuiTab: {
wrapper: {
flexDirection: 'row',
},
},
},
});
export const App: FC = () => ( return (
<Router>
<div data-testid="App"> <div data-testid="App">
<Switch> <ThemeProvider theme={muiTheme}>
<Route path={NonAuthRoutes.signIn} component={AuthUser} /> <AuthContext.Provider value={value}>
<Route exact path={NonAuthRoutes.home} component={LandingPage} /> <Router>
<PrivateRoute <Switch>
path={AuthRoutes.dashboard} <Route exact path={NonAuthRoutes.home} component={LandingPage} />
Component={HomePage} <Route path={NonAuthRoutes.auth} component={AuthUser} />
requiredRoles={[Roles.admin, Roles.operator, Roles.senior]} <Route path={AuthRoutes.dashboard} component={All(Dashboard)} />
/> <Route
<PrivateRoute path={NonAuthRoutes.unauthorized}
path={AuthRoutes.profile} component={Unauthorized}
Component={ProfilePage} />
requiredRoles={[Roles.admin, Roles.operator, Roles.senior]} <Route component={NotFound} />
/> </Switch>
<Route path={NonAuthRoutes.unauthorized} component={Unauthorized} /> </Router>
<Route component={NotFound} /> </AuthContext.Provider>
</Switch> </ThemeProvider>
</div> </div>
</Router> );
); };
import axios from 'axios';
/**
* Config for django compatibility.
*/
export const configDjangoCookieName = (): void => {
axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.withCredentials = true;
};
import axios from 'axios';
/**
* @async
* Call for csrf cookie. This cookie is the user session identifier and
* must be sent during the login process.
* @returns {string} csrf cookie
*/
export const fetchCookie = async (): Promise<string> =>
axios('/api/web/csrf').then((res) => res.data.token);
import axios from 'axios';
export const getReservations = async (): Promise<any> =>
axios('/api/web/reservations/').then((res) => res.data);
import axios from 'axios';
/**
* @async
* Asks for the current set role.
* @returns {string} empty if the role is not set or a name between those defined
* in {@see userRoles}
*/
export const getRole = async (): Promise<string> =>
axios('/api/web/login/get_role').then((res) => res.data.role);
import axios from 'axios';
/**
* @async
* Ask the server all the roles of an user if the user has multiple roles.
* @returns {string[]} array of roles.
*/
export const getRoles = async (): Promise<string[]> =>
axios('/api/web/login/get_roles').then((res) => res.data.roles);
import axios from 'axios';
/**
* @async
* Ask the server if the user is authenticated.
* @returns {boolean} true or false if the user is authenticated.
*/
export const isAuthenticated = async (): Promise<boolean> =>
axios('/api/web/login/is_authenticated').then(
(res) => res.data.is_authenticated,
);
...@@ -5,7 +5,10 @@ ...@@ -5,7 +5,10 @@
*/ */
export enum AuthRoutes { export enum AuthRoutes {
dashboard = '/dashboard', dashboard = '/dashboard',
home = '/home',
profile = '/profile', profile = '/profile',
reservation = '/reservation',
choseRole = '/choseRole',
} }
/** /**
...@@ -13,6 +16,8 @@ export enum AuthRoutes { ...@@ -13,6 +16,8 @@ export enum AuthRoutes {
*/ */
export enum NonAuthRoutes { export enum NonAuthRoutes {
home = '/', home = '/',
auth = '/auth',
signIn = '/signIn', signIn = '/signIn',
signUp = '/signUp',
unauthorized = '/unauthorized', unauthorized = '/unauthorized',
} }
import axios from 'axios';
/**
* @async
* Set the role of the user in the database.
* @return {string} role
*/
export const setRole = async (role: string): Promise<void> =>
axios.post('/api/web/login/set_role', { role });
export enum Roles { export enum Roles {
/** Website visitor, is not logged. Can access to NonAuthRoutes */
visitor = '',
/** Can access to routes wrapped by withAuthorization. */
senior = 'senior', senior = 'senior',
admin = 'admin', admin = 'admin',
operator = 'operator', operator = 'operator',
driver = 'driver',
} }
import React, { FC } from 'react';
import Container from '@material-ui/core/Container';
import { Redirect, Route, useRouteMatch } from 'react-router-dom';
import { AuthRoutes, NonAuthRoutes } from 'api/routes';
import { ChoseRole } from 'components/Auth/ChoseRole/ChoseRole';
import { SignInForm } from 'components/Auth/SignInForm/SignInForm';
import { SignUpForm } from 'components/Auth/SignUpForm/SignUpForm';
/**
* Keeps all components related to SignIn/SignOut logic.
*/
export const AuthUser: FC = () => {
const { path } = useRouteMatch();
return (
<Container maxWidth="sm">
<Route path={`${path}${NonAuthRoutes.signIn}`} component={SignInForm} />
<Route path={`${path}${NonAuthRoutes.signUp}`} component={SignUpForm} />
<Route path={`${path}${AuthRoutes.choseRole}`} component={ChoseRole} />
</Container>
);
};
import { Roles } from 'api/userRoles';
import { createContext } from 'react';
export type AuthData = {
role: string;
isAuth: boolean | null;
setRole: (role: string) => void;
setIsAuth: (state: boolean | null) => void;
};
export const AuthContext = createContext<AuthData>({
role: Roles.visitor,
isAuth: false,
setRole: () => {
// Do nothing
},
setIsAuth: () => {
// Do nothing
},
});
Chose role.
```
const role = ["operator", "driver",];
<ChoseRole role={role} />
```
\ No newline at end of file
import React from 'react';
import { render } from '@testing-library/react';
import { ChoseRole } from './ChoseRole';
describe('<ChoseRole />', () => {
it('renders without crashing', () => {
const wrapper = render(<ChoseRole />);
expect(wrapper.queryByTestId('ChoseRole')).toBeTruthy();
});
});
import React, { FC, useContext, useEffect, useState } from 'react';
import Button from '@material-ui/core/Button';
import { getRoles } from 'api/getRoles';
import { setRole } from 'api/setRole';
import { useHistory } from 'react-router-dom';
import { AuthRoutes } from 'api/routes';
import { AuthContext } from 'components/Auth/AuthContext';
/**
* Page that let's users decide role between available roles.
* This is intended to use when a user has more than one role.
* @returns ChoseRole component.
*/
export const ChoseRole: FC = () => {
const history = useHistory();
const [userRoles, setUserRoles] = useState<string[]>(['']);
const { setIsAuth } = useContext(AuthContext);
const choseAndForward = (role: string): void => {
// Set role in the server.
setRole(role);
setIsAuth(true);
// Push to homepage.
history.push(`${AuthRoutes.dashboard}${AuthRoutes.home}`);
};
useEffect(() => {
getRoles().then((fetchedRoles) => setUserRoles(fetchedRoles));
}, []);
return (
<div data-testid="ChoseRole">
{userRoles ? (
userRoles.map((role) => (
<Button
variant="outlined"
color="default"
key={role}
onClick={() => choseAndForward(role)}
>
{role}
</Button>
))
) : (
<h1>No roles were returned.</h1>
)}
</div>
);
};
...@@ -15,11 +15,11 @@ type Props = { ...@@ -15,11 +15,11 @@ type Props = {
* Takes error from react-hook-form and is thrown if * Takes error from react-hook-form and is thrown if
* the component is not valid. * the component is not valid.
*/ */
error: boolean; error?: boolean;
/** /**
* Message to display if the component is not valid. * Message to display if the component is not valid.
*/ */
errorMessage: string; errorMessage?: string;
/** /**
* React-hook-form control. * React-hook-form control.
*/ */
...@@ -27,11 +27,29 @@ type Props = { ...@@ -27,11 +27,29 @@ type Props = {
/** /**
* Validation rules. * Validation rules.
*/ */
rules: Partial<unknown>; rules?: Partial<unknown>;
/**
* HTML type of input.
*/
type: string;
/**
* MUI props override for defining style on fly.
*/
InputLabelProps?: Record<string, boolean>;
}; };
export const InputField: FC<Props> = (props: Props) => { export const InputField: FC<Props> = (props: Props) => {
const { name, label, error, errorMessage, control, rules } = props; const {
name,
label,
error,
type,
errorMessage,
control,
rules,
InputLabelProps,
} = props;
return ( return (
<Controller <Controller
name={name} name={name}
...@@ -41,7 +59,7 @@ export const InputField: FC<Props> = (props: Props) => { ...@@ -41,7 +59,7 @@ export const InputField: FC<Props> = (props: Props) => {
<TextField <TextField
variant="outlined" variant="outlined"
margin="normal" margin="normal"
type={name} type={type}
required required
fullWidth fullWidth
id={name} id={name}
...@@ -53,8 +71,16 @@ export const InputField: FC<Props> = (props: Props) => { ...@@ -53,8 +71,16 @@ export const InputField: FC<Props> = (props: Props) => {
autoFocus autoFocus
error={error} error={error}
helperText={error && errorMessage} helperText={error && errorMessage}
InputLabelProps={{ ...InputLabelProps, shrink: true }}
/> />
)} )}
/> />
); );
}; };
InputField.defaultProps = {
rules: undefined,
error: false,
errorMessage: '',
InputLabelProps: {},
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment