From 2eeb58ca0c63d6d1c4d56bd30ec2d7bd9297c6b4 Mon Sep 17 00:00:00 2001 From: marlenetienne Date: Mon, 10 Feb 2025 14:21:11 +0100 Subject: [PATCH] feat: ANT-2726 - Link with gaia (#56) * feat: ANT-2726 - Link with gaia * fix: env variables * fix: modify env variable * fix: env variables * fix: remove console log * fix: use litteral * fix:test getEnv * fix: add authority * fix: variable * fix: variables 3 * fix: variable 4 * fix: variable 5 * fix: variable 6 * fix: variable 7 * fix: variable 8 * fix: use const for config * fix: var authority * fix: var authority 2 * fix: env variable * fix: env values --------- Co-authored-by: marlenetienne Co-authored-by: vargastat --- package.json | 1 + src/App.tsx | 69 +++++++++++-------- .../common/handler/ThemeHandler.tsx | 4 +- .../common/handler/test/ThemeHandler.test.tsx | 10 +-- .../layout/stdAvatarGroup/StdAvatarGroup.tsx | 7 +- .../layout/stdAvatarGroup/avatarTools.ts | 12 ++-- src/envVariables.ts | 8 ++- src/hooks/test/useFetchProjectList.test.ts | 37 +++++----- src/hooks/test/useStudyTableDisplay.test.ts | 9 ++- src/hooks/useFetchProjectList.ts | 29 +++++--- src/hooks/useHandlePinnedProjectList.ts | 4 +- src/hooks/useStudyTableDisplay.ts | 13 ++-- src/mocks/data/list/user.mocks.ts | 4 +- .../projectDetails/ProjectDetails.tsx | 8 +-- src/pages/pegase/settings/Settings.tsx | 6 +- src/pages/pegase/studies/KeywordsInput.tsx | 18 ++--- src/pages/pegase/studies/ProjectInput.tsx | 4 +- src/shared/const/authConfig.ts | 23 +++++++ src/shared/services/authService.ts | 40 +++++++++++ src/shared/services/pinnedProjectService.ts | 17 ++--- src/shared/services/projectService.ts | 53 +++++++++----- src/shared/services/studyService.ts | 42 +++++------ .../test/pinnedProjectService.test.tsx | 4 +- .../services/test/projectService.test.tsx | 23 +++---- .../services/test/studyService.test.tsx | 3 +- src/shared/types/common/User.type.ts | 7 +- src/shared/utils/authUtils.ts | 18 +++++ src/store/contexts/ProjectContext.tsx | 4 +- src/store/contexts/UserContext.tsx | 12 ++-- src/store/contexts/UserProvider.tsx | 41 +++++++++++ src/store/contexts/UserSettingsContext.tsx | 14 ++++ vite-env.d.ts | 6 ++ 32 files changed, 372 insertions(+), 178 deletions(-) create mode 100644 src/shared/const/authConfig.ts create mode 100644 src/shared/services/authService.ts create mode 100644 src/shared/utils/authUtils.ts create mode 100644 src/store/contexts/UserProvider.tsx create mode 100644 src/store/contexts/UserSettingsContext.tsx diff --git a/package.json b/package.json index 55b57d1..d3e89a6 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "i18next": "^23.14.0", "mutative": "^1.0.6", "postcss": "^8.4.39", + "oidc-client-ts": "^3.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.0.3", diff --git a/src/App.tsx b/src/App.tsx index 55d8fd3..027e311 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,12 +4,12 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Suspense } from 'react'; +import { Suspense, useEffect } from 'react'; import { Route, Routes } from 'react-router-dom'; import './App.css'; import ThemeHandler from './components/common/handler/ThemeHandler'; import PegaseStar from './components/pegase/star/PegaseStar'; -import { UserContext } from '@/store/contexts/UserContext'; +import { UserSettingsContext } from '@/store/contexts/UserSettingsContext.tsx'; import { THEME_COLOR } from '@/shared/types'; import { menuBottomData, menuTopData } from './routes'; import { PegaseToastContainer } from './shared/notification/containers'; @@ -19,36 +19,51 @@ import { RdsNavbar } from 'rte-design-system-react'; import { navBarConfig } from '@/shared/const/navBarConfig'; import { useTranslation } from 'react-i18next'; import { translateMenuItemLabel } from '@/shared/utils/textUtils.ts'; +import { PEGASE_NAVBAR_ID } from '@/shared/constants.ts'; +import UserProvider from '@/store/contexts/UserProvider.tsx'; +import { AuthService } from '@/shared/services/authService.ts'; function App() { const { t } = useTranslation(); + useEffect(() => { + const handleAuth = async () => { + if (window.location.href.includes('code=')) { + await AuthService.handleCallback(); + window.location.replace('/'); // Redirect to home page after login + } + }; + void handleAuth(); + }, []); + return ( -
- - - - -
- - - - } /> - } /> - {Object.entries([...menuBottomData, ...menuTopData]).map(([key, route]) => ( - - ))} - - -
-
-
+ +
+ + + + +
+ + + + } /> + } /> + {Object.entries([...menuBottomData, ...menuTopData]).map(([key, route]) => ( + + ))} + + +
+
+
+
); } diff --git a/src/components/common/handler/ThemeHandler.tsx b/src/components/common/handler/ThemeHandler.tsx index 3c30596..7d0d3e3 100644 --- a/src/components/common/handler/ThemeHandler.tsx +++ b/src/components/common/handler/ThemeHandler.tsx @@ -4,14 +4,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { UserContext } from '@/store/contexts/UserContext'; +import { UserSettingsContext } from '@/store/contexts/UserSettingsContext.tsx'; import usePrevious from '@/hooks/common/usePrevious'; import { THEME_COLOR } from '@/shared/types'; import { useEffect } from 'react'; const ThemeHandler = () => { const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)'); - const themeColor = UserContext.useStore((store) => store.theme); + const themeColor = UserSettingsContext.useStore((store) => store.theme); const previousTheme = usePrevious(themeColor, undefined); useEffect(() => { if (previousTheme) { diff --git a/src/components/common/handler/test/ThemeHandler.test.tsx b/src/components/common/handler/test/ThemeHandler.test.tsx index c53ba63..5060069 100644 --- a/src/components/common/handler/test/ThemeHandler.test.tsx +++ b/src/components/common/handler/test/ThemeHandler.test.tsx @@ -8,12 +8,12 @@ import { render } from '@testing-library/react'; import { describe, expect, it, Mock, vi } from 'vitest'; import { THEME_COLOR } from '@/shared/types'; import ThemeHandler from '../ThemeHandler'; -import { UserContext } from '@/store/contexts/UserContext'; +import { UserSettingsContext } from '@/store/contexts/UserSettingsContext.tsx'; import usePrevious from '@/hooks/common/usePrevious'; -// Mocking the UserContext and usePrevious hook -vi.mock('@/store/contexts/UserContext', () => ({ - UserContext: { +// Mocking the UserSettingsContext and usePrevious hook +vi.mock('@/store/contexts/UserSettingsContext', () => ({ + UserSettingsContext: { useStore: vi.fn(), }, })); @@ -23,7 +23,7 @@ vi.mock('@/hooks/common/usePrevious', () => ({ })); describe('ThemeHandler', () => { - const mockUseStore = UserContext.useStore as Mock; + const mockUseStore = UserSettingsContext.useStore as Mock; const mockUsePrevious = usePrevious as Mock; beforeEach(() => { diff --git a/src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx b/src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx index 5981c4a..e295ce0 100644 --- a/src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx +++ b/src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx @@ -4,15 +4,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { User } from '@/shared/types/common/User.type'; -import StdAvatar from '../stdAvatar/StdAvatar'; -import { AvatarSize } from '../stdAvatar/StdAvatar'; +import { UserInfo } from '@/shared/types/common/User.type'; +import StdAvatar, { AvatarSize } from '../stdAvatar/StdAvatar'; import { classBuilder as groupContainerClassBuilder } from './avatarGroupClassBuilder'; import { getColor, getUserFullname, getUserInitials, splitUserList } from './avatarTools'; import { useRdsId } from 'rte-design-system-react'; type StdAvatarGroup = { - users: User[]; + users: UserInfo[]; avatarSize: AvatarSize; id?: string; }; diff --git a/src/components/common/layout/stdAvatarGroup/avatarTools.ts b/src/components/common/layout/stdAvatarGroup/avatarTools.ts index 21cb9a5..f53544c 100644 --- a/src/components/common/layout/stdAvatarGroup/avatarTools.ts +++ b/src/components/common/layout/stdAvatarGroup/avatarTools.ts @@ -4,13 +4,13 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { User } from '@/shared/types/common/User.type'; +import { UserInfo } from '@/shared/types/common/User.type'; export const AVATAR_COLORS = ['green', 'purple', 'blue', 'pink', 'gray', 'orange'] as const; const USER_SEPARATOR = ' - '; const MAX_USER_CHIP = 3; -export const splitUserList = (users: User[]) => { +export const splitUserList = (users: UserInfo[]) => { if (users.length <= MAX_USER_CHIP) { return users; } @@ -18,19 +18,19 @@ export const splitUserList = (users: User[]) => { return [firstUser, secondUser, otherUsers]; }; -export const getInitials = (user: User) => { +export const getInitials = (user: UserInfo) => { const [firstName, lastName = ''] = user.fullname.split(' '); return lastName.charAt(0) + firstName.charAt(0); }; -export const getUserInitials = (users: User | User[]) => { +export const getUserInitials = (users: UserInfo | UserInfo[]) => { if (!Array.isArray(users)) { return getInitials(users); } return `+${users.length}`; }; -export const getUserFullname = (users: User | User[]) => { +export const getUserFullname = (users: UserInfo | UserInfo[]) => { if (!Array.isArray(users)) { return users.fullname; } @@ -38,7 +38,7 @@ export const getUserFullname = (users: User | User[]) => { }; //assign a random color from COLORS -export const getColor = (users: User | User[]) => { +export const getColor = (users: UserInfo | UserInfo[]) => { if (!Array.isArray(users)) { return AVATAR_COLORS[ users.fullname.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % AVATAR_COLORS.length diff --git a/src/envVariables.ts b/src/envVariables.ts index 883d455..8ce41bb 100644 --- a/src/envVariables.ts +++ b/src/envVariables.ts @@ -6,11 +6,17 @@ type EnvVariableType = { VITE_BACK_END_BASE_URL: string; + VITE_OAUTH2_CLIENT_ID: string; + VITE_OAUTH2_REDIRECT_URL: string; + VITE_OAUTH2_AUTHORITY: string; }; // Environment Variable Template to Be Replaced at Runtime export const envVariables: EnvVariableType = { VITE_BACK_END_BASE_URL: '${URL_BACKEND}', + VITE_OAUTH2_CLIENT_ID: '${PEGASE_OAUTH2_CLIENT_ID}', + VITE_OAUTH2_REDIRECT_URL: '${PEGASE_OAUTH2_REDIRECT_URL}', + VITE_OAUTH2_AUTHORITY: '${PEGASE_OAUTH2_AUTHORITY}', }; -export const getEnvVariables = (key: keyof EnvVariableType) => +export const getEnvVariables = (key: keyof EnvVariableType): string => envVariables[key].startsWith('$') ? (import.meta.env[key] as string) : envVariables[key]; diff --git a/src/hooks/test/useFetchProjectList.test.ts b/src/hooks/test/useFetchProjectList.test.ts index 87836d4..ef16793 100644 --- a/src/hooks/test/useFetchProjectList.test.ts +++ b/src/hooks/test/useFetchProjectList.test.ts @@ -16,23 +16,22 @@ vi.mock('@/envVariables', () => ({ describe('useFetchProjectList', () => { beforeEach(() => { - global.fetch = vi.fn(() => - Promise.resolve({ - json: () => - Promise.resolve({ - content: [ - { - projectId: '1', - name: 'Project 1', - tags: ['Tag1', 'Tag2'], - creationDate: '2023-10-01', - createdBy: 'User A', - }, - ], - totalElements: 1, - }), - }), - ) as unknown as typeof fetch; + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => + Promise.resolve({ + content: [ + { + projectId: '1', + name: 'Project 1', + tags: ['Tag1', 'Tag2'], + creationDate: '2023-10-01', + createdBy: 'User A', + }, + ], + totalElements: 1, + }), + }); }); afterEach(() => { @@ -60,7 +59,7 @@ describe('useFetchProjectList', () => { renderHook(() => useFetchProjectList('test', 0, 9)); await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=1&size=9&search=test'); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=1&size=9&search=test', {}); }); }); @@ -68,7 +67,7 @@ describe('useFetchProjectList', () => { renderHook(() => useFetchProjectList('', 1, 9)); await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=2&size=9&search='); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=2&size=9&search=', {}); }); }); }); diff --git a/src/hooks/test/useStudyTableDisplay.test.ts b/src/hooks/test/useStudyTableDisplay.test.ts index 28bda89..66d10f3 100644 --- a/src/hooks/test/useStudyTableDisplay.test.ts +++ b/src/hooks/test/useStudyTableDisplay.test.ts @@ -15,6 +15,7 @@ vi.mock('@/envVariables', () => ({ describe('useStudyTableDisplay', () => { beforeEach(() => { global.fetch = vi.fn(); + vi.restoreAllMocks(); }); afterEach(() => { @@ -48,11 +49,11 @@ describe('useStudyTableDisplay', () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, - json: async () => mockResponse, + json: async () => Promise.resolve(mockResponse), }); const { result } = renderHook(() => - useStudyTableDisplay({ searchTerm: 'test', sortBy: { status: 'desc' }, reloadStudies: true }), + useStudyTableDisplay({ searchTerm: 'test', sortBy: { status: 'desc' }, reloadStudies: false }), ); await waitFor(() => { expect(result.current.rows).toHaveLength(2); @@ -61,16 +62,18 @@ describe('useStudyTableDisplay', () => { //expect(global.fetch).toHaveBeenCalledTimes(1); TODO: ANT-2719 expect(global.fetch).toHaveBeenCalledWith( 'https://mockapi.com/v1/study/search?page=1&size=9&projectId=&search=test&sortColumn=status&sortDirection=desc', + {}, ); }); await act(async () => { - renderHook(() => useStudyTableDisplay({ searchTerm: 'mouad', sortBy: { project: 'asc' }, reloadStudies: true })); + renderHook(() => useStudyTableDisplay({ searchTerm: 'mouad', sortBy: { project: 'asc' }, reloadStudies: false })); }); //expect(global.fetch).toHaveBeenCalledTimes(1); TODO: ANT-2719 expect(global.fetch).toHaveBeenCalledWith( 'https://mockapi.com/v1/study/search?page=1&size=9&projectId=&search=mouad&sortColumn=project&sortDirection=asc', + {}, ); }); diff --git a/src/hooks/useFetchProjectList.ts b/src/hooks/useFetchProjectList.ts index 0970502..54b491c 100644 --- a/src/hooks/useFetchProjectList.ts +++ b/src/hooks/useFetchProjectList.ts @@ -9,6 +9,7 @@ import { ProjectActionType, ProjectInfo } from '@/shared/types/pegase/Project.ty import { fetchProjectFromSearchTerm } from '@/shared/services/projectService.ts'; import { PROJECT_ACTION } from '@/shared/enum/project.ts'; import { useProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; +import { PaginatedResponse } from '@/shared/types'; export const useFetchProjectList = (searchTerm: string, current: number, intervalSize: number) => { const [projects, setProjects] = useState([]); @@ -16,17 +17,23 @@ export const useFetchProjectList = (searchTerm: string, current: number, interva const dispatch = useProjectDispatch(); const fetchProjects = useCallback( - async (searchTerm: string, current: number, intervalSize: number) => { - fetchProjectFromSearchTerm(searchTerm, current, intervalSize) - .then((json) => { - dispatch?.({ - type: PROJECT_ACTION.INIT_PROJECT_LIST, - payload: json.content, - } as ProjectActionType); - setProjects(json.content); - setCount(json.totalElements); - }) - .catch((error) => console.error(error)); + async (term: string, currentPage: number, size: number) => { + try { + const { content, totalElements } = (await fetchProjectFromSearchTerm( + term, + currentPage, + size, + )) as PaginatedResponse; + + dispatch?.({ + type: PROJECT_ACTION.INIT_PROJECT_LIST, + payload: content, + } as ProjectActionType); + setProjects(content); + setCount(totalElements); + } catch (error) { + console.error(error); + } }, [current, intervalSize, searchTerm], ); diff --git a/src/hooks/useHandlePinnedProjectList.ts b/src/hooks/useHandlePinnedProjectList.ts index cb03f93..ca891b8 100644 --- a/src/hooks/useHandlePinnedProjectList.ts +++ b/src/hooks/useHandlePinnedProjectList.ts @@ -5,7 +5,7 @@ */ import { useCallback, useEffect } from 'react'; -import { ProjectActionType } from '@/shared/types/pegase/Project.type'; +import { ProjectActionType, ProjectInfo } from '@/shared/types/pegase/Project.type'; import { fetchPinnedProjects, pinProject, unpinProject } from '@/shared/services/pinnedProjectService'; import { v4 as uuidv4 } from 'uuid'; import { dismissToast, notifyToast, NotifyWithActionProps } from '@/shared/notification/notification.tsx'; @@ -20,7 +20,7 @@ export const useHandlePinnedProjectList = () => { const getPinnedProjects = useCallback(async () => { try { - const projects = await fetchPinnedProjects(userId); + const projects = (await fetchPinnedProjects(userId)) as ProjectInfo[]; if (projects?.length) { dispatch?.({ type: PROJECT_ACTION.INIT_PINNED_PROJECT_LIST, diff --git a/src/hooks/useStudyTableDisplay.ts b/src/hooks/useStudyTableDisplay.ts index 6bb827e..7bcdcc4 100644 --- a/src/hooks/useStudyTableDisplay.ts +++ b/src/hooks/useStudyTableDisplay.ts @@ -5,7 +5,7 @@ */ import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; -import { StudyDTO } from '@/shared/types'; +import { PaginatedResponse, StudyDTO } from '@/shared/types'; import { fetchSearchStudies } from '@/shared/services/studyService.ts'; const ITEMS_PER_PAGE = 9; @@ -37,7 +37,7 @@ export const useStudyTableDisplay = ({ const [rows, setRows] = useState([]); const [count, setCount] = useState(0); const [currentPage, setCurrentPage] = useState(0); - const [error, setError] = useState(null); + const [errorValue, setErrorValue] = useState(null); const searchTermRef = useRef(searchTerm); const projectIdRef = useRef(projectId); @@ -57,12 +57,13 @@ export const useStudyTableDisplay = ({ useEffect(() => { fetchSearchStudies(searchTermRef.current, projectIdRef.current, currentPage, intervalSize, sortByRef.current) - .then(({ content, totalElements }) => { + .then((json) => { + const { content, totalElements } = json as PaginatedResponse; setRows(content); setCount(totalElements); }) - .catch((error) => setError(error)); - }, [currentPage, searchTerm, projectId, sortBy, reloadStudies]); + .catch((error: unknown) => setErrorValue(error as Error)); + }, [currentPage, searchTermRef, projectIdRef, sortByRef, reloadStudies]); - return { rows, count, intervalSize, currentPage, setPage: setCurrentPage, error }; + return { rows, count, intervalSize, currentPage, setPage: setCurrentPage, error: errorValue }; }; diff --git a/src/mocks/data/list/user.mocks.ts b/src/mocks/data/list/user.mocks.ts index 7128087..15874c4 100644 --- a/src/mocks/data/list/user.mocks.ts +++ b/src/mocks/data/list/user.mocks.ts @@ -6,9 +6,9 @@ import { randomNumber } from '@/mocks/mockTools'; import { LIST_FIRSTNAME, LIST_NAME } from './names'; -import { User } from '@/shared/types/common/User.type'; +import { UserInfo } from '@/shared/types/common/User.type'; -export const generateFixedUser = (userIdx: number, seed = 1): User => { +export const generateFixedUser = (userIdx: number, seed = 1): UserInfo => { const firstName = LIST_FIRSTNAME[userIdx % LIST_FIRSTNAME.length]; const name = LIST_NAME[(userIdx + seed) % LIST_FIRSTNAME.length]; return { diff --git a/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx b/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx index 58a3a78..97c9077 100644 --- a/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx +++ b/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx @@ -37,12 +37,12 @@ const ProjectDetails = () => { const [projectInfo, setProjectDetails] = useState({} as ProjectInfo); const location = useLocation(); - const { projectId } = location.state || {}; + const projectId = location.state?.projectId as string | null; useEffect(() => { - const getProjectDetails = async (projectId: string) => { + const getProjectDetails = async (id: string) => { try { - const data = await fetchProjectDetails(projectId); + const data = (await fetchProjectDetails(id)) as ProjectInfo; setProjectDetails({ id: data.id, @@ -57,7 +57,7 @@ const ProjectDetails = () => { studies: [], }); } catch (error) { - console.error(`Error retrieving project details: ${projectId}`, error); + console.error(`Error retrieving project details: ${id}`, error); } }; if (projectId && !projectInfo.id) { diff --git a/src/pages/pegase/settings/Settings.tsx b/src/pages/pegase/settings/Settings.tsx index b1666e2..ea521a0 100644 --- a/src/pages/pegase/settings/Settings.tsx +++ b/src/pages/pegase/settings/Settings.tsx @@ -4,15 +4,15 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { UserContext } from '@/store/contexts/UserContext'; +import { UserSettingsContext } from '@/store/contexts/UserSettingsContext.tsx'; import { THEME_COLOR } from '@/shared/types'; import i18next from 'i18next'; import { useTranslation } from 'react-i18next'; import { RdsSwitch } from 'rte-design-system-react'; const Settings = () => { - const themeColor = UserContext.useStore((store) => store.theme); - const setContext = UserContext.useSetStore(); + const themeColor = UserSettingsContext.useStore((store) => store.theme); + const setContext = UserSettingsContext.useSetStore(); const { i18n } = useTranslation(); const changeLanguageHandler = (lang: string) => { void i18n.changeLanguage(lang); diff --git a/src/pages/pegase/studies/KeywordsInput.tsx b/src/pages/pegase/studies/KeywordsInput.tsx index ec733dd..1111a02 100644 --- a/src/pages/pegase/studies/KeywordsInput.tsx +++ b/src/pages/pegase/studies/KeywordsInput.tsx @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import { RdsButton, RdsIcon, RdsIconId, RdsInputText } from 'rte-design-system-react'; import { fetchSuggestedKeywords } from '@/shared/services/studyService'; import { clsx } from 'clsx'; @@ -12,21 +12,21 @@ import { useTranslation } from 'react-i18next'; interface KeywordsInputProps { keywords: string[]; - setKeywords: React.Dispatch>; + setKeywords: Dispatch>; maxNbKeywords?: number; maxNbCharacters?: number; minNbCharacters?: number; width?: string; } -const KeywordsInput: React.FC = ({ +const KeywordsInput = ({ keywords, setKeywords, maxNbKeywords, maxNbCharacters, minNbCharacters, width, -}) => { +}: KeywordsInputProps) => { const { t } = useTranslation(); const [keywordInput, setKeywordInput] = useState(''); const [errorMessage, setErrorMessage] = useState(''); @@ -39,7 +39,7 @@ const KeywordsInput: React.FC = ({ setKeywordInput(value); setErrorMessage(''); // Clear error message when input changes try { - const tags = await fetchSuggestedKeywords(value); + const tags = (await fetchSuggestedKeywords(value)) as string[]; setSuggestedKeywords(tags); } catch (error) { setErrorMessage('Failed to fetch suggested keywords'); @@ -47,7 +47,7 @@ const KeywordsInput: React.FC = ({ }; const handleAddKeyword = (suggestedKeyword = keywordInput) => { - if (suggestedKeyword.trim()) { + if (keywords?.length > 0 && suggestedKeyword.trim()) { if (keywords.includes(suggestedKeyword.trim())) { setErrorMessage(t('projectModal.@keyword_already_exists')); } else if ( @@ -84,7 +84,7 @@ const KeywordsInput: React.FC = ({ if (!input) { return false; } else { - if (minNbCharacters) { + if (minNbCharacters && !maxNbCharacters) { return input.length >= minNbCharacters; } else if (minNbCharacters && maxNbCharacters) { return input.length >= minNbCharacters && input.length <= maxNbCharacters; @@ -104,7 +104,7 @@ const KeywordsInput: React.FC = ({ onChange={handleKeywordChange} placeHolder="Add a keyword" variant="outlined" - maxLength={maxNbCharacters as number | undefined} + maxLength={maxNbCharacters} /> {shouldAddKeywordButton(keywordInput) && ( @@ -164,7 +164,7 @@ const KeywordsInput: React.FC = ({ {/* Clear All Keywords Button */} - {keywords.length > 0 && ( + {keywords?.length > 0 && (
Clear all diff --git a/src/pages/pegase/studies/ProjectInput.tsx b/src/pages/pegase/studies/ProjectInput.tsx index e7daa89..5965980 100644 --- a/src/pages/pegase/studies/ProjectInput.tsx +++ b/src/pages/pegase/studies/ProjectInput.tsx @@ -22,7 +22,7 @@ const ProjectInput: React.FC = ({ value, onChange }) => { useEffect(() => { const loadProjects = async () => { try { - const projectList = await fetchProjectsFromPartialName(value); + const projectList = (await fetchProjectsFromPartialName(value)) as string[]; setProjects(projectList); } catch (error) { setErrorMessage('Failed to fetch projects'); @@ -30,7 +30,7 @@ const ProjectInput: React.FC = ({ value, onChange }) => { }; if (value) { - loadProjects(); + void loadProjects(); } else { setProjects([]); } diff --git a/src/shared/const/authConfig.ts b/src/shared/const/authConfig.ts new file mode 100644 index 0000000..3bff489 --- /dev/null +++ b/src/shared/const/authConfig.ts @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { getEnvVariables } from '@/envVariables.ts'; + +interface AuthConfig { + authority: string; + client_id: string; + redirect_uri: string; + scope: string; + maxExpiresIn: number; +} + +export const config: AuthConfig = { + client_id: getEnvVariables('VITE_OAUTH2_CLIENT_ID'), + redirect_uri: getEnvVariables('VITE_OAUTH2_REDIRECT_URL'), + authority: getEnvVariables('VITE_OAUTH2_AUTHORITY'), + scope: 'openid email profile', + maxExpiresIn: 600, +}; diff --git a/src/shared/services/authService.ts b/src/shared/services/authService.ts new file mode 100644 index 0000000..24f9cce --- /dev/null +++ b/src/shared/services/authService.ts @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { User, UserManager } from 'oidc-client-ts'; +import { config } from '@/shared/const/authConfig'; + +const userManager = new UserManager(config); + +export const AuthService = { + login: async () => await userManager.signinRedirect(), + refresh: () => userManager.signinSilent(), + logout: () => userManager.signoutRedirect(), + getUser: async (): Promise => await userManager.getUser(), + handleCallback: () => userManager.signinRedirectCallback(), + + getAccessToken: async (): Promise => { + const user = await userManager.getUser(); + return user?.access_token || null; + }, + + authFetch: async (url: string, options: RequestInit = {}): Promise => { + const token = await AuthService.getAccessToken(); + if (token) { + if (options.headers instanceof Headers) { + options.headers.append('Authorization', `Bearer ${token}`); + } else if (Array.isArray(options.headers)) { + options.headers.push(['Authorization', `Bearer ${token}`]); + } else { + options.headers = { + ...options.headers, + Authorization: `Bearer ${token}`, + }; + } + } + return await fetch(url, options); + }, +}; diff --git a/src/shared/services/pinnedProjectService.ts b/src/shared/services/pinnedProjectService.ts index 3355b55..ca09bac 100644 --- a/src/shared/services/pinnedProjectService.ts +++ b/src/shared/services/pinnedProjectService.ts @@ -6,6 +6,7 @@ import { PROJECT_PIN_ENDPOINT, PROJECT_PINNED_ENDPOINT, PROJECT_UNPIN_ENDPOINT } from '@/shared/const/apiEndPoint'; import { ProjectInfo } from '@/shared/types/pegase/Project.type'; +import { AuthService } from '@/shared/services/authService.ts'; /** * Retrieve pinned projects list by user id @@ -13,21 +14,21 @@ import { ProjectInfo } from '@/shared/types/pegase/Project.type'; * @param {string} userId - User id * @returns {Promise} - Promise object that represents a list of projects */ -export const fetchPinnedProjects = async (userId: string) => { +export const fetchPinnedProjects = async (userId: string): Promise => { const apiUrl = `${PROJECT_PINNED_ENDPOINT}?userId=${userId}`; - const response = await fetch(apiUrl); + const response = await AuthService.authFetch(apiUrl); if (!response?.ok) { throw new Error('Failed to fetch project details'); } - const json = await response.json(); + const json = (await response.json()) as Partial[]; return json.map((project: Partial) => ({ ...project, projectId: project.id?.toString(), pinned: project.pinned ?? true, - })); + })) as ProjectInfo[]; }; /** @@ -39,11 +40,11 @@ export const fetchPinnedProjects = async (userId: string) => { * @return {Promise} - Object that describes a project */ -export const pinProject = async (projectId: string) => { +export const pinProject = async (projectId: string): Promise => { const userId = 'me00247'; const apiUrl = `${PROJECT_PIN_ENDPOINT}?userId=${userId}&projectId=${projectId}`; - const response = await fetch(apiUrl, { + const response = await AuthService.authFetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -55,7 +56,7 @@ export const pinProject = async (projectId: string) => { throw new Error(`${errorText}`); } - return await response.json(); + return (await response.json()) as ProjectInfo; }; /** @@ -67,7 +68,7 @@ export const pinProject = async (projectId: string) => { export const unpinProject = async (userId: string, projectId: string) => { const apiUrl = `${PROJECT_UNPIN_ENDPOINT}?userId=${userId}&projectId=${projectId}`; - const response = await fetch(apiUrl, { + const response = await AuthService.authFetch(apiUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/src/shared/services/projectService.ts b/src/shared/services/projectService.ts index 10c4b79..2433782 100644 --- a/src/shared/services/projectService.ts +++ b/src/shared/services/projectService.ts @@ -6,9 +6,17 @@ import { PROJECT_AUTOCOMPLETE_ENDPOINT, PROJECT_ENDPOINT, PROJECT_SEARCH_ENDPOINT } from '@/shared/const/apiEndPoint'; import { ProjectInfo, ProjectResponse } from '@/shared/types/pegase/Project.type'; +import { AuthService } from '@/shared/services/authService.ts'; +import { PaginatedResponse } from '@/shared/types'; -export const deleteProjectById = async (projectId: string) => { - const response = await fetch(`${PROJECT_ENDPOINT}/${projectId}`, { +/** + * Delete project + * + * @param {string} projectId + * @return {Promise} + */ +export const deleteProjectById = async (projectId: string): Promise => { + const response = await AuthService.authFetch(`${PROJECT_ENDPOINT}/${projectId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', @@ -17,7 +25,7 @@ export const deleteProjectById = async (projectId: string) => { if (!response.ok) { const errorText = await response.text(); - const errorData = JSON.parse(errorText); + const errorData = JSON.parse(errorText) as Error; throw new Error(`${errorData.message || errorText}`); } }; @@ -26,30 +34,30 @@ export const deleteProjectById = async (projectId: string) => { * Retrieve details of a project * * @param {string} projectId - Project id - * @return {Promise} - Project details + * @return {Promise} - Project details */ -export const fetchProjectDetails = async (projectId: string) => { - const response = await fetch(`${PROJECT_ENDPOINT}/${projectId}`); +export const fetchProjectDetails = async (projectId: string): Promise => { + const response = await AuthService.authFetch(`${PROJECT_ENDPOINT}/${projectId}`); if (!response?.ok) { throw new Error('Failed to fetch project details'); } - return await response.json(); + return (await response.json()) as ProjectInfo; }; /** * Retrieve a project from a partial name of project * * @param {string} query - Partial name of a project - * @return {Promise} - List of project name + * @return {Promise} - List of project name */ -export const fetchProjectsFromPartialName = async (query: string): Promise => { - const response = await fetch(`${PROJECT_AUTOCOMPLETE_ENDPOINT}?partialName=${query}`); +export const fetchProjectsFromPartialName = async (query: string): Promise => { + const response = await AuthService.authFetch(`${PROJECT_AUTOCOMPLETE_ENDPOINT}?partialName=${query}`); if (!response.ok) { throw new Error('Failed to fetch projects'); } - const data = await response.json(); + const data = (await response.json()) as ProjectInfo[]; return data.map((project: { name: string }) => project.name); }; @@ -59,13 +67,24 @@ export const fetchProjectsFromPartialName = async (query: string): Promise | Error>} */ -export const fetchProjectFromSearchTerm = async (searchTerm: string, current: number, intervalSize: number) => { - const response = await fetch( +export const fetchProjectFromSearchTerm = async ( + searchTerm: string, + current: number, + intervalSize: number, +): Promise | Error> => { + const response = await AuthService.authFetch( `${PROJECT_SEARCH_ENDPOINT}?page=${current + 1}&size=${intervalSize}&search=${searchTerm || ''}`, ); - return await response.json(); + if (!response.ok) { + const errorText = await response.text(); + const errorData = JSON.parse(errorText) as Error; + throw new Error(`${errorData.message}`); + } + + return (await response.json()) as PaginatedResponse; }; /** @@ -79,7 +98,7 @@ export const createProject = async ( ): Promise => { const apiUrl = `${PROJECT_ENDPOINT}`; - const response = await fetch(apiUrl, { + const response = await AuthService.authFetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -89,8 +108,8 @@ export const createProject = async ( if (!response.ok) { const errorText = await response.text(); - const errorData = JSON.parse(errorText); + const errorData = JSON.parse(errorText) as Error; throw new Error(`${errorData.message}`); } - return await response.json(); + return (await response.json()) as ProjectResponse; }; diff --git a/src/shared/services/studyService.ts b/src/shared/services/studyService.ts index ef00bd7..1cb3e09 100644 --- a/src/shared/services/studyService.ts +++ b/src/shared/services/studyService.ts @@ -8,6 +8,7 @@ import { PaginatedResponse, StudyDTO } from '@/shared/types'; import { STUDY_SEARCH_ENDPOINT } from '@/shared/const/apiEndPoint'; import { STUDY_ENDPOINT, STUDY_KEYWORDS_SEARCH_ENDPOINT } from '@/shared/const/apiEndPoint.ts'; import { notifyToast } from '@/shared/notification/notification.tsx'; +import { AuthService } from '@/shared/services/authService.ts'; /** * Retrieve a list of studies from a term @@ -18,15 +19,15 @@ import { notifyToast } from '@/shared/notification/notification.tsx'; * @param {number} intervalSize - Number of items per page * @param {{ [key: string]: 'asc' | 'desc' })} sortBy - Object that describes the sorting type (ascending or descending) of a column * - * @returns {Promise>} - Promise object that represents a list of studies + * @return {Promise | Error>} - Promise object that represents a list of studies */ export const fetchSearchStudies = async ( - searchTerm = '', - projectId = '', - currentPage = 0, - intervalSize = 0, + searchTerm: string = '', + projectId: string = '', + currentPage: number = 0, + intervalSize: number = 0, sortBy?: { [key: string]: 'asc' | 'desc' }, -): Promise> => { +): Promise | Error> => { let entries: [string, 'asc' | 'desc'] | null = null; if (sortBy && JSON.stringify(sortBy) !== '{}') { entries = Object.entries(sortBy)[0]; @@ -34,11 +35,11 @@ export const fetchSearchStudies = async ( const apiUrl = `${STUDY_SEARCH_ENDPOINT}?page=${currentPage + 1}&size=${intervalSize}&projectId=${projectId}&search=${searchTerm}&sortColumn=${entries?.[0] ?? ''}&sortDirection=${entries?.[1] ?? ''}`; - const response = await fetch(apiUrl); + const response = await AuthService.authFetch(apiUrl); if (!response.ok) { throw new Error('Failed to fetch user studies'); } - const json: PaginatedResponse = await response.json(); + const json = (await response.json()) as PaginatedResponse; return { content: json.content, totalElements: json.totalElements }; }; @@ -47,15 +48,14 @@ export const fetchSearchStudies = async ( * Retrieve a list of suggested keywords from a partial name of a study * * @param {string} query - Partial name of a study - * - * @returns {Promise} - Promise object that represents a list of keywords + * @return {Promise} - Promise object that represents a list of keywords */ -export const fetchSuggestedKeywords = async (query: string): Promise => { - const response = await fetch(`${STUDY_KEYWORDS_SEARCH_ENDPOINT}?partialName=${query}`); +export const fetchSuggestedKeywords = async (query: string): Promise => { + const response = await AuthService.authFetch(`${STUDY_KEYWORDS_SEARCH_ENDPOINT}?partialName=${query}`); if (!response.ok) { throw new Error('Failed to fetch suggested keywords'); } - return await response.json(); + return (await response.json()) as string[]; }; /** @@ -63,10 +63,11 @@ export const fetchSuggestedKeywords = async (query: string): Promise = * Display toast if creation succeeds or fails * * @param {Omit} studyData - Partial study data + * @return {Promise} */ -export const saveStudy = async (studyData: Omit) => { +export const saveStudy = async (studyData: Omit): Promise => { try { - const response = await fetch(`${STUDY_ENDPOINT}`, { + const response = await AuthService.authFetch(`${STUDY_ENDPOINT}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -75,7 +76,7 @@ export const saveStudy = async (studyData: Omit} */ -export const deleteStudy = async (id: number) => { +export const deleteStudy = async (id: number): Promise => { try { - const response = await fetch(`${STUDY_ENDPOINT}/${id}`, { + const response = await AuthService.authFetch(`${STUDY_ENDPOINT}/${id}`, { method: 'DELETE', }); if (!response.ok) { @@ -111,10 +113,10 @@ export const deleteStudy = async (id: number) => { type: 'success', message: 'Study deleted successfully', }); - } catch (error: any) { + } catch (error: unknown) { notifyToast({ type: 'error', - message: `${error.message}`, + message: `${(error as Error).message}`, }); } }; diff --git a/src/shared/services/test/pinnedProjectService.test.tsx b/src/shared/services/test/pinnedProjectService.test.tsx index 9a3026c..6f785e6 100644 --- a/src/shared/services/test/pinnedProjectService.test.tsx +++ b/src/shared/services/test/pinnedProjectService.test.tsx @@ -107,7 +107,7 @@ describe('fetchPinnedProjects', () => { await waitFor(() => { expect(global.fetch).toHaveBeenCalledTimes(1); - expect(global.fetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/pinned?userId=${userId}`); + expect(global.fetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/pinned?userId=${userId}`, {}); expect(result).toEqual(mockResponse); }); }); @@ -160,7 +160,7 @@ describe('unpinProject', () => { // Failed fetch response moc global.fetch = vi.fn().mockResolvedValueOnce({ ok: false, - text: async () => 'Error message', + text: async () => Promise.resolve('Error message'), }); await expect(async () => unpinProject(userId, projectId)).rejects.toThrowError('Error message'); diff --git a/src/shared/services/test/projectService.test.tsx b/src/shared/services/test/projectService.test.tsx index 8a49018..fffc07d 100644 --- a/src/shared/services/test/projectService.test.tsx +++ b/src/shared/services/test/projectService.test.tsx @@ -25,9 +25,7 @@ describe('deleteProjectById', () => { global.fetch = vi.fn(); vi.clearAllMocks(); vi.stubGlobal('JSON', { - parse: (text: string) => { - return { message: text }; - }, + parse: (text: string) => ({ message: text }), stringify: (text: string) => text, }); }); @@ -97,7 +95,7 @@ describe('fetchProjectDetails', () => { await waitFor(() => { expect(global.fetch).toHaveBeenCalledTimes(1); - expect(global.fetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/${projectId}`); + expect(global.fetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/${projectId}`, {}); }); }); @@ -123,9 +121,7 @@ describe('fetchProjectsFromPartialName', () => { global.fetch = vi.fn(); vi.clearAllMocks(); vi.stubGlobal('JSON', { - parse: (text: string) => { - return { message: text }; - }, + parse: (text: string) => ({ message: text }), stringify: (text: string) => text, }); }); @@ -138,8 +134,8 @@ describe('fetchProjectsFromPartialName', () => { it('should delete a pinned project from pinned project list', async () => { global.fetch = vi.fn().mockResolvedValueOnce({ ok: true, - json: () => { - return Promise.resolve([ + json: () => + Promise.resolve([ { id: '123', name: 'Bilan prévisionnel 2023', @@ -156,15 +152,14 @@ describe('fetchProjectsFromPartialName', () => { creationDate: '2013-08-01', tags: ['tag3', 'tag4'], }, - ]); - }, + ]), }); const result = await fetchProjectsFromPartialName('name'); await waitFor(() => { expect(global.fetch).toHaveBeenCalledTimes(1); - expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/autocomplete?partialName=name'); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/autocomplete?partialName=name', {}); expect(result).toEqual(['Bilan prévisionnel 2023', 'Bilan prévisionnel 2019']); }); }); @@ -188,9 +183,7 @@ describe('createProject', () => { global.fetch = vi.fn(); vi.clearAllMocks(); vi.stubGlobal('JSON', { - parse: (text: string) => { - return { message: text }; - }, + parse: (text: string) => ({ message: text }), stringify: (text: string) => text, }); }); diff --git a/src/shared/services/test/studyService.test.tsx b/src/shared/services/test/studyService.test.tsx index 1a82ec1..fd0d00d 100644 --- a/src/shared/services/test/studyService.test.tsx +++ b/src/shared/services/test/studyService.test.tsx @@ -53,6 +53,7 @@ describe('fetchSearchStudies', () => { expect(global.fetch).toHaveBeenCalledTimes(1); expect(global.fetch).toHaveBeenCalledWith( `https://mockapi.com/v1/study/search?page=4&size=10&projectId=124&search=test&sortColumn=column&sortDirection=asc`, + {}, ); expect(result).toEqual(mockResponse); }); @@ -90,7 +91,7 @@ describe('fetchSuggestedKeywords', () => { const result = await fetchSuggestedKeywords('test'); expect(global.fetch).toHaveBeenCalledTimes(1); - expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/study/keywords/search?partialName=test'); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/study/keywords/search?partialName=test', {}); expect(result).toEqual(mockResponse); }); diff --git a/src/shared/types/common/User.type.ts b/src/shared/types/common/User.type.ts index c3af83e..b80b1af 100644 --- a/src/shared/types/common/User.type.ts +++ b/src/shared/types/common/User.type.ts @@ -3,11 +3,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { User } from 'oidc-client-ts'; -export interface User { +export interface UserInfo { id: string; nni: string; fullname: string; email: string; isAdmin?: boolean; } + +export interface UserState { + user: User | null; +} diff --git a/src/shared/utils/authUtils.ts b/src/shared/utils/authUtils.ts new file mode 100644 index 0000000..e422c41 --- /dev/null +++ b/src/shared/utils/authUtils.ts @@ -0,0 +1,18 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { User } from 'oidc-client-ts'; + +type ProfileWithRole = User & { + profile: { + realm_access: { + roles: string[]; + }; + }; +}; + +export const hasUserRole = (role: string, user: ProfileWithRole): boolean => + user?.profile.realm_access.roles?.includes(role); diff --git a/src/store/contexts/ProjectContext.tsx b/src/store/contexts/ProjectContext.tsx index c11b507..70b687f 100644 --- a/src/store/contexts/ProjectContext.tsx +++ b/src/store/contexts/ProjectContext.tsx @@ -8,9 +8,9 @@ import { createContext, Dispatch, ReactNode, useContext, useReducer } from 'reac import { ProjectActionType, ProjectState } from '@/shared/types/pegase/Project.type'; import projectReducer from '@/store/reducers/projectReducer'; -const initialValue: ProjectState = { projects: [], pinnedProjects: [] }; +const initialState: ProjectState = { projects: [], pinnedProjects: [] }; -export const ProjectContext = createContext(initialValue); +export const ProjectContext = createContext(initialState); export const ProjectDispatchContext = createContext | null>(null); export const useProject = () => useContext(ProjectContext); diff --git a/src/store/contexts/UserContext.tsx b/src/store/contexts/UserContext.tsx index 04687ea..e4e5e41 100644 --- a/src/store/contexts/UserContext.tsx +++ b/src/store/contexts/UserContext.tsx @@ -4,11 +4,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { THEME_COLOR } from '@/shared/types'; -import createFastContext from './createFastContext'; +import { createContext, useContext } from 'react'; +import { UserState } from '@/shared/types'; -export type UserContextStore = { - theme: THEME_COLOR; +const initialState: UserState = { + user: null, }; - -export const UserContext = createFastContext(); +export const UserContext = createContext(initialState); +export const useUser = () => useContext(UserContext); diff --git a/src/store/contexts/UserProvider.tsx b/src/store/contexts/UserProvider.tsx new file mode 100644 index 0000000..0f12fd5 --- /dev/null +++ b/src/store/contexts/UserProvider.tsx @@ -0,0 +1,41 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { ReactNode, useEffect, useState } from 'react'; +import { UserState } from '@/shared/types'; +import { AuthService } from '@/shared/services/authService'; +import { UserContext } from './UserContext'; + +export interface UserProviderProps { + children: ReactNode; + initialValue: UserState; +} + +const UserProvider = ({ children, initialValue }: UserProviderProps) => { + const [user, setUser] = useState(initialValue); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const getUser = async () => { + const userInfo = await AuthService.getUser(); + if (!userInfo) { + // Redirection automatique vers Keycloak pour l'authentification + await AuthService.login(); + } else { + setUser({ user: userInfo }); + setLoading(false); + } + }; + void getUser(); + }, []); + + if (loading) { + return
Loading...
; // Affiche un message de chargement pendant la vérification + } + return {children}; +}; + +export default UserProvider; diff --git a/src/store/contexts/UserSettingsContext.tsx b/src/store/contexts/UserSettingsContext.tsx new file mode 100644 index 0000000..29fbc72 --- /dev/null +++ b/src/store/contexts/UserSettingsContext.tsx @@ -0,0 +1,14 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { THEME_COLOR } from '@/shared/types'; +import createFastContext from './createFastContext'; + +export type UserSettingsContextStore = { + theme: THEME_COLOR; +}; + +export const UserSettingsContext = createFastContext(); diff --git a/vite-env.d.ts b/vite-env.d.ts index bbe913a..9729546 100644 --- a/vite-env.d.ts +++ b/vite-env.d.ts @@ -1 +1,7 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + declare const APP_VERSION: string;