diff --git a/src/assets/icones/keycloak.svg b/src/assets/icones/keycloak.svg new file mode 100644 index 000000000..62de2e59f --- /dev/null +++ b/src/assets/icones/keycloak.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Routes/AutoLogoutContainer.jsx b/src/components/Routes/AutoLogoutContainer.jsx index 695aa98ab..5b9661bb7 100644 --- a/src/components/Routes/AutoLogoutContainer.jsx +++ b/src/components/Routes/AutoLogoutContainer.jsx @@ -10,7 +10,7 @@ import { close as closeAction, open as openAction } from 'state/autoLogout' import { useAppDispatch, useAppSelector } from 'state' import { logout as logoutAction } from 'state/me' -import { ACCES_TOKEN, BACK_API_URL, REFRESH_TOKEN, REFRESH_TOKEN_INTERVAL, SESSION_TIMEOUT } from '../../constants' +import { ACCESS_TOKEN, BACK_API_URL, REFRESH_TOKEN, REFRESH_TOKEN_INTERVAL, SESSION_TIMEOUT } from '../../constants' import useStyles from './styles' @@ -78,7 +78,7 @@ const AutoLogoutContainer = () => { const res = await axios.post(`${BACK_API_URL}/accounts/refresh/`) if (res.status === 200) { - localStorage.setItem(ACCES_TOKEN, res.data.access) + localStorage.setItem(ACCESS_TOKEN, res.data.access) localStorage.setItem(REFRESH_TOKEN, res.data.refresh) } else { logout() diff --git a/src/components/Routes/LeftSideBar/LeftSideBar.tsx b/src/components/Routes/LeftSideBar/LeftSideBar.tsx index 705e7a448..1aeaa4c45 100644 --- a/src/components/Routes/LeftSideBar/LeftSideBar.tsx +++ b/src/components/Routes/LeftSideBar/LeftSideBar.tsx @@ -151,7 +151,6 @@ const LeftSideBar: React.FC<{ open?: boolean }> = (props) => { > { - localStorage.clear() dispatch(logoutAction()) navigate('/') }} @@ -170,7 +169,6 @@ const LeftSideBar: React.FC<{ open?: boolean }> = (props) => { { - localStorage.clear() dispatch(logoutAction()) navigate('/') }} diff --git a/src/components/Routes/PrivateRoute.tsx b/src/components/Routes/PrivateRoute.tsx index 7204633e5..c1fbeea3f 100644 --- a/src/components/Routes/PrivateRoute.tsx +++ b/src/components/Routes/PrivateRoute.tsx @@ -5,7 +5,7 @@ import { gql } from 'apollo-boost' import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material' -import { ACCES_TOKEN } from '../../constants' +import { ACCESS_TOKEN } from '../../constants' import { useAppSelector, useAppDispatch } from '../../state' import { login } from '../../state/me' @@ -24,7 +24,7 @@ const PrivateRoute: React.FC = () => { const me = useAppSelector((state) => state.me) const dispatch = useAppDispatch() const location = useLocation() - const authToken = localStorage.getItem(ACCES_TOKEN) + const authToken = localStorage.getItem(ACCESS_TOKEN) const [allowRedirect, setRedirection] = useState(false) diff --git a/src/constants.js b/src/constants.js index 707af65a7..4992e84c1 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,8 +1,21 @@ export let BOOLEANTRUE = 'true' -export const ACCES_TOKEN = 'access' +export const ACCESS_TOKEN = 'access' export const REFRESH_TOKEN = 'refresh' +export const OIDC_PROVIDER_URL = import.meta.env.DEV + ? import.meta.env.VITE_OIDC_PROVIDER_URL + : '{VITE_OIDC_PROVIDER_URL}' +export const OIDC_REDIRECT_URI = import.meta.env.DEV + ? import.meta.env.VITE_OIDC_REDIRECT_URI + : '{VITE_OIDC_REDIRECT_URI}' +export const OIDC_RESPONSE_TYPE = import.meta.env.DEV + ? import.meta.env.VITE_OIDC_RESPONSE_TYPE + : '{VITE_OIDC_RESPONSE_TYPE}' +export const OIDC_CLIENT_ID = import.meta.env.DEV ? import.meta.env.VITE_OIDC_CLIENT_ID : '{VITE_OIDC_CLIENT_ID}' +export const OIDC_SCOPE = import.meta.env.DEV ? import.meta.env.VITE_OIDC_SCOPE : '{VITE_OIDC_SCOPE}' +export const OIDC_STATE = import.meta.env.DEV ? import.meta.env.VITE_OIDC_STATE : '{VITE_OIDC_STATE}' + export const BACK_API_URL = import.meta.env.DEV ? import.meta.env.VITE_BACK_API_URL : '{VITE_BACK_API_URL}' export const REQUEST_API_URL = import.meta.env.DEV ? import.meta.env.VITE_REQUEST_API_URL : '{VITE_REQUEST_API_URL}' export const FHIR_API_URL = import.meta.env.DEV ? import.meta.env.VITE_FHIR_API_URL : '{VITE_FHIR_API_URL}' diff --git a/src/services/aphp/servicePractitioner.ts b/src/services/aphp/servicePractitioner.ts index 8641ec4d6..9c1096825 100644 --- a/src/services/aphp/servicePractitioner.ts +++ b/src/services/aphp/servicePractitioner.ts @@ -1,5 +1,6 @@ import { AxiosResponse } from 'axios' import apiBackend from 'services/apiBackend' +import { ACCESS_TOKEN, REFRESH_TOKEN } from '../../constants' export interface IServicePractitioner { /** @@ -11,7 +12,8 @@ export interface IServicePractitioner { * * Retourne la reponse de Axios */ - authenticate: (username: string, password: string) => Promise + authenticateWithCredentials: (username: string, password: string) => Promise + authenticateWithCode: (code: string) => Promise /** * Cette fonction permet d'appeler la route de logout @@ -46,7 +48,7 @@ export interface IServicePractitioner { } const servicePractitioner: IServicePractitioner = { - authenticate: async (username, password) => { + authenticateWithCredentials: async (username, password) => { try { const formData = new FormData() formData.append('username', username.toString()) @@ -54,12 +56,27 @@ const servicePractitioner: IServicePractitioner = { return await apiBackend.post(`/accounts/login/`, formData) } catch (error) { - console.error("erreur lors de l'exécution de la fonction authenticate", error) + console.error('Error authenticating with credentials', error) + return error + } + }, + + authenticateWithCode: async (authCode: string) => { + try { + const res = await apiBackend.post(`/auth/oidc/login`, { auth_code: authCode }) + if (res.status === 200) { + localStorage.setItem(ACCESS_TOKEN, res.data.jwt.access) + localStorage.setItem(REFRESH_TOKEN, res.data.jwt.refresh) + } + return res + } catch (error) { + console.error('Error authenticating with an authorization code', error) return error } }, logout: async () => { + localStorage.clear() await apiBackend.post(`/accounts/logout/`) }, diff --git a/src/services/apiBackend.js b/src/services/apiBackend.js index cf85c51a7..4539f803e 100644 --- a/src/services/apiBackend.js +++ b/src/services/apiBackend.js @@ -1,5 +1,5 @@ import axios from 'axios' -import { ACCES_TOKEN, BACK_API_URL } from '../constants' +import { ACCESS_TOKEN, BACK_API_URL } from '../constants' const apiBackend = axios.create({ baseURL: BACK_API_URL, @@ -9,7 +9,7 @@ const apiBackend = axios.create({ }) apiBackend.interceptors.request.use((config) => { - const token = localStorage.getItem(ACCES_TOKEN) + const token = localStorage.getItem(ACCESS_TOKEN) config.headers.Authorization = `Bearer ${token}` return config }) diff --git a/src/services/apiFhir.js b/src/services/apiFhir.js index 7df62f725..c273f4857 100644 --- a/src/services/apiFhir.js +++ b/src/services/apiFhir.js @@ -1,5 +1,5 @@ import axios from 'axios' -import { ACCES_TOKEN, FHIR_API_URL } from '../constants' +import { ACCESS_TOKEN, FHIR_API_URL } from '../constants' const apiFhir = axios.create({ baseURL: FHIR_API_URL, @@ -10,7 +10,7 @@ const apiFhir = axios.create({ }) apiFhir.interceptors.request.use((config) => { - const token = localStorage.getItem(ACCES_TOKEN) + const token = localStorage.getItem(ACCESS_TOKEN) config.headers.Authorization = `Bearer ${token}` return config }) diff --git a/src/views/Login/Login.jsx b/src/views/Login/Login.jsx index cd9068e70..d9cbff64f 100644 --- a/src/views/Login/Login.jsx +++ b/src/views/Login/Login.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import localforage from 'localforage' import { @@ -21,15 +21,27 @@ import NoRights from 'components/ErrorView/NoRights' import logo from 'assets/images/logo-login.png' import logoAPHP from 'assets/images/logo-aphp.png' +import { ReactComponent as Keycloak } from 'assets/icones/keycloak.svg' import { useAppDispatch } from 'state' import { login as loginAction } from 'state/me' -import { ACCES_TOKEN, REFRESH_TOKEN } from '../../constants' +import { + ACCESS_TOKEN, + REFRESH_TOKEN, + OIDC_CLIENT_ID, + OIDC_PROVIDER_URL, + OIDC_REDIRECT_URI, + OIDC_RESPONSE_TYPE, + OIDC_SCOPE, + OIDC_STATE +} from '../../constants' import services from 'services/aphp' import useStyles from './styles' import { getDaysLeft } from '../../utils/formatDate' +import Welcome from '../Welcome/Welcome' +import clsx from 'clsx' const ErrorSnackBarAlert = ({ open, setError, errorMessage }) => { const _setError = () => { @@ -97,11 +109,19 @@ const Login = () => { const [error, setError] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [open, setOpen] = useState(false) + const [authCode, setAuthCode] = useState(undefined) + const urlParams = new URLSearchParams(window.location.search) + const code = urlParams.get('code') - React.useEffect(() => { + useEffect(() => { localforage.setItem('persist:root', '') + if (code) setAuthCode(code) }, []) + useEffect(() => { + if (authCode) login() + }, [authCode]) + const getPractitionerData = async (practitioner, lastConnection, maintenance, accessExpirations = []) => { if (practitioner) { const practitionerPerimeters = await services.perimeters.getPerimeters() @@ -169,12 +189,19 @@ const Login = () => { if (loading) return setLoading(true) - if (!username || !password) { - setLoading(false) - return setError(true), setErrorMessage("L'un des champs nom d'utilisateur ou mot de passe est vide.") - } + let response = null - const response = await services.practitioner.authenticate(username, password) + if (authCode) { + response = await services.practitioner.authenticateWithCode(authCode) + } else { + if (!username || !password) { + setLoading(false) + return setError(true), setErrorMessage("L'un des champs nom d'utilisateur ou mot de passe est vide.") + } + if (username && password) { + response = await services.practitioner.authenticateWithCredentials(username, password) + } + } if (!response) { setLoading(false) @@ -214,10 +241,10 @@ const Login = () => { const { status, data = {} } = response if (status === 200) { - localStorage.setItem(ACCES_TOKEN, data.jwt.access) + localStorage.setItem(ACCESS_TOKEN, data.jwt.access) localStorage.setItem(REFRESH_TOKEN, data.jwt.refresh) - const practitioner = await services.practitioner.fetchPractitioner(username) + const practitioner = await services.practitioner.fetchPractitioner(data.user.provider_username) if (!practitioner || practitioner.error || !practitioner.response || practitioner.response.status !== 200) { setLoading(false) @@ -266,9 +293,28 @@ const Login = () => { login() } + const oidcLogin = (e) => { + e.preventDefault() + window.location = + `${OIDC_PROVIDER_URL}?state=${OIDC_STATE}&` + // eslint-disable-line + `client_id=${OIDC_CLIENT_ID}&` + // eslint-disable-line + `redirect_uri=${OIDC_REDIRECT_URI}&` + // eslint-disable-line + `response_type=${OIDC_RESPONSE_TYPE}&` + // eslint-disable-line + `scope=${OIDC_SCOPE}` // eslint-disable-line + } + if (noRights === true) return - return ( + return code ? ( + + + Connexion... + + + + ) : authCode ? ( + + ) : ( <> @@ -332,6 +378,17 @@ const Login = () => { > {loading ? : 'Connexion'} + {/* */} diff --git a/src/views/Login/styles.ts b/src/views/Login/styles.ts index cbd0b6597..9cb126f91 100644 --- a/src/views/Login/styles.ts +++ b/src/views/Login/styles.ts @@ -13,7 +13,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ marginBottom: theme.spacing(2) }, bienvenue: { - fontSize: '15px' + fontSize: 15 }, image: { backgroundImage: `url(${BackgroundLogin})`, @@ -22,15 +22,26 @@ const useStyles = makeStyles()((theme: Theme) => ({ backgroundPosition: 'center' }, submit: { - margin: theme.spacing(2, 0, 5), + margin: theme.spacing(2, 0, 0), backgroundColor: '#5BC5F2', color: 'white', - height: '50px', - width: '185px', - borderRadius: '25px' + height: 50, + width: 185, + borderRadius: 25 + }, + oidcButton: { + backgroundColor: '#153D8A', + width: 250 }, mention: { - marginTop: '8px' + marginTop: 8 + }, + oidcConnexionProgress: { + margin: '10%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + rowGap: 30 } }))