diff --git a/src/frontend/apps/impress/src/features/auth/__tests__/useAuthStore.test.tsx b/src/frontend/apps/impress/src/features/auth/__tests__/utils.test.tsx similarity index 86% rename from src/frontend/apps/impress/src/features/auth/__tests__/useAuthStore.test.tsx rename to src/frontend/apps/impress/src/features/auth/__tests__/utils.test.tsx index 02814677..2aa0a02c 100644 --- a/src/frontend/apps/impress/src/features/auth/__tests__/useAuthStore.test.tsx +++ b/src/frontend/apps/impress/src/features/auth/__tests__/utils.test.tsx @@ -1,7 +1,7 @@ import { Crisp } from 'crisp-sdk-web'; import fetchMock from 'fetch-mock'; -import { useAuthStore } from '../useAuthStore'; +import { gotoLogout } from '../utils'; jest.mock('crisp-sdk-web', () => ({ ...jest.requireActual('crisp-sdk-web'), @@ -17,7 +17,7 @@ jest.mock('crisp-sdk-web', () => ({ }, })); -describe('useAuthStore', () => { +describe('utils', () => { afterEach(() => { jest.clearAllMocks(); fetchMock.restore(); @@ -33,7 +33,7 @@ describe('useAuthStore', () => { writable: true, }); - useAuthStore.getState().logout(); + gotoLogout(); expect(Crisp.session.reset).toHaveBeenCalled(); }); diff --git a/src/frontend/apps/impress/src/features/auth/api/index.ts b/src/frontend/apps/impress/src/features/auth/api/index.ts index d81520d1..ce8db5d4 100644 --- a/src/frontend/apps/impress/src/features/auth/api/index.ts +++ b/src/frontend/apps/impress/src/features/auth/api/index.ts @@ -1,2 +1,2 @@ -export * from './getMe'; +export * from './useAuthQuery'; export * from './types'; diff --git a/src/frontend/apps/impress/src/features/auth/api/getMe.tsx b/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx similarity index 61% rename from src/frontend/apps/impress/src/features/auth/api/getMe.tsx rename to src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx index 4c02c27b..714cc9e0 100644 --- a/src/frontend/apps/impress/src/features/auth/api/getMe.tsx +++ b/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx @@ -1,4 +1,6 @@ -import { fetchAPI } from '@/api'; +import { UseQueryOptions, useQuery } from '@tanstack/react-query'; + +import { APIError, fetchAPI } from '@/api'; import { User } from './types'; @@ -19,3 +21,16 @@ export const getMe = async (): Promise => { } return response.json() as Promise; }; + +export const KEY_AUTH = 'auth'; + +export function useAuthQuery( + queryConfig?: UseQueryOptions, +) { + return useQuery({ + queryKey: [KEY_AUTH], + queryFn: getMe, + staleTime: 1000 * 60 * 15, // 15 minutes + ...queryConfig, + }); +} diff --git a/src/frontend/apps/impress/src/features/auth/components/Auth.tsx b/src/frontend/apps/impress/src/features/auth/components/Auth.tsx index a4746fbe..714ef86f 100644 --- a/src/frontend/apps/impress/src/features/auth/components/Auth.tsx +++ b/src/frontend/apps/impress/src/features/auth/components/Auth.tsx @@ -1,10 +1,10 @@ import { Loader } from '@openfun/cunningham-react'; -import { useRouter } from 'next/router'; -import { PropsWithChildren, useEffect, useState } from 'react'; +import { PropsWithChildren } from 'react'; import { Box } from '@/components'; +import HomeContent from '@/features/home/components/HomeContent'; -import { useAuthStore } from '../stores/useAuthStore'; +import { useAuth } from '../hooks'; /** * TODO: Remove this restriction when we will have a homepage design for non-authenticated users. @@ -14,47 +14,10 @@ import { useAuthStore } from '../stores/useAuthStore'; * When we will have a homepage design for non-authenticated users, we will remove this restriction to have * the full website accessible without authentication. */ -const regexpUrlsAuth = [/\/docs\/$/g, /^\/$/g]; - export const Auth = ({ children }: PropsWithChildren) => { - const { initAuth, initiated, authenticated, login, getAuthUrl } = - useAuthStore(); - const { asPath, replace } = useRouter(); + const { user, isLoading, pathAllowed } = useAuth(); - const [pathAllowed, setPathAllowed] = useState( - !regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)), - ); - - useEffect(() => { - initAuth(); - }, [initAuth]); - - useEffect(() => { - setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp))); - }, [asPath]); - - // We force to login except on allowed paths - useEffect(() => { - if (!initiated || authenticated || pathAllowed) { - return; - } - - login(); - }, [authenticated, pathAllowed, login, initiated]); - - // Redirect to the path before login - useEffect(() => { - if (!authenticated) { - return; - } - - const authUrl = getAuthUrl(); - if (authUrl) { - void replace(authUrl); - } - }, [authenticated, getAuthUrl, replace]); - - if ((!initiated && pathAllowed) || (!authenticated && !pathAllowed)) { + if (isLoading) { return ( diff --git a/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx b/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx index 1eecfc63..decfb5d7 100644 --- a/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx +++ b/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx @@ -1,22 +1,23 @@ import { Button } from '@openfun/cunningham-react'; import { useTranslation } from 'react-i18next'; -import { useAuthStore } from '@/features/auth'; +import { useAuth } from '../hooks'; +import { gotoLogin, gotoLogout } from '../utils'; export const ButtonLogin = () => { const { t } = useTranslation(); - const { logout, authenticated, login } = useAuthStore(); + const { authenticated } = useAuth(); if (!authenticated) { return ( - ); } return ( - ); diff --git a/src/frontend/apps/impress/src/features/auth/conf.ts b/src/frontend/apps/impress/src/features/auth/conf.ts index 63a3701f..b8e59cf5 100644 --- a/src/frontend/apps/impress/src/features/auth/conf.ts +++ b/src/frontend/apps/impress/src/features/auth/conf.ts @@ -1 +1,5 @@ +import { baseApiUrl } from '@/api'; + export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth'; +export const LOGIN_URL = `${baseApiUrl()}authenticate/`; +export const LOGOUT_URL = `${baseApiUrl()}logout/`; diff --git a/src/frontend/apps/impress/src/features/auth/hooks/index.ts b/src/frontend/apps/impress/src/features/auth/hooks/index.ts new file mode 100644 index 00000000..d9ae7204 --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/hooks/index.ts @@ -0,0 +1 @@ +export * from './useAuth'; diff --git a/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx b/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx new file mode 100644 index 00000000..6a13d514 --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx @@ -0,0 +1,34 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + +import { useAuthQuery } from '../api'; +import { getAuthUrl } from '../utils'; + +const regexpUrlsAuth = [/\/docs\/$/g, /\/docs$/g, /^\/$/g]; + +export const useAuth = () => { + const { data: user, ...authStates } = useAuthQuery(); + const { pathname, replace } = useRouter(); + + const [pathAllowed, setPathAllowed] = useState( + !regexpUrlsAuth.some((regexp) => !!pathname.match(regexp)), + ); + + useEffect(() => { + setPathAllowed(!regexpUrlsAuth.some((regexp) => !!pathname.match(regexp))); + }, [pathname]); + + // Redirect to the path before login + useEffect(() => { + if (!user) { + return; + } + + const authUrl = getAuthUrl(); + if (authUrl) { + void replace(authUrl); + } + }, [user, replace]); + + return { user, authenticated: !!user, pathAllowed, ...authStates }; +}; diff --git a/src/frontend/apps/impress/src/features/auth/index.ts b/src/frontend/apps/impress/src/features/auth/index.ts index b1b8f696..e2501124 100644 --- a/src/frontend/apps/impress/src/features/auth/index.ts +++ b/src/frontend/apps/impress/src/features/auth/index.ts @@ -1,3 +1,4 @@ export * from './api/types'; export * from './components'; -export * from './stores'; +export * from './hooks'; +export * from './utils'; diff --git a/src/frontend/apps/impress/src/features/auth/stores/index.ts b/src/frontend/apps/impress/src/features/auth/stores/index.ts deleted file mode 100644 index 86014de6..00000000 --- a/src/frontend/apps/impress/src/features/auth/stores/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useAuthStore'; diff --git a/src/frontend/apps/impress/src/features/auth/stores/useAuthStore.tsx b/src/frontend/apps/impress/src/features/auth/stores/useAuthStore.tsx deleted file mode 100644 index 8761789e..00000000 --- a/src/frontend/apps/impress/src/features/auth/stores/useAuthStore.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { create } from 'zustand'; - -import { baseApiUrl } from '@/api'; -import { terminateCrispSession } from '@/services'; - -import { User, getMe } from '../api'; -import { PATH_AUTH_LOCAL_STORAGE } from '../conf'; - -interface AuthStore { - initiated: boolean; - authenticated: boolean; - initAuth: () => void; - logout: () => void; - login: () => void; - setAuthUrl: (url: string) => void; - getAuthUrl: () => string | undefined; - userData?: User; -} - -const initialState = { - initiated: false, - authenticated: false, - userData: undefined, -}; - -export const useAuthStore = create((set, get) => ({ - initiated: initialState.initiated, - authenticated: initialState.authenticated, - userData: initialState.userData, - initAuth: () => { - getMe() - .then((data: User) => { - set({ authenticated: true, userData: data }); - }) - .catch(() => {}) - .finally(() => { - set({ initiated: true }); - }); - }, - login: () => { - get().setAuthUrl(window.location.pathname); - - window.location.replace(`${baseApiUrl()}authenticate/`); - }, - logout: () => { - terminateCrispSession(); - window.location.replace(`${baseApiUrl()}logout/`); - }, - // If we try to access a specific page and we are not authenticated - // we store the path in the local storage to redirect to it after login - setAuthUrl() { - if (window.location.pathname !== '/') { - localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname); - } - }, - // If a path is stored in the local storage, we return it then remove it - getAuthUrl() { - const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE); - if (path_auth) { - localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE); - return path_auth; - } - }, -})); diff --git a/src/frontend/apps/impress/src/features/auth/utils.ts b/src/frontend/apps/impress/src/features/auth/utils.ts new file mode 100644 index 00000000..766505eb --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/utils.ts @@ -0,0 +1,27 @@ +import { terminateCrispSession } from '@/services/Crisp'; + +import { LOGIN_URL, LOGOUT_URL, PATH_AUTH_LOCAL_STORAGE } from './conf'; + +export const getAuthUrl = () => { + const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE); + if (path_auth) { + localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE); + return path_auth; + } +}; + +export const setAuthUrl = () => { + if (window.location.pathname !== '/') { + localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname); + } +}; + +export const gotoLogin = () => { + setAuthUrl(); + window.location.replace(LOGIN_URL); +}; + +export const gotoLogout = () => { + terminateCrispSession(); + window.location.replace(LOGOUT_URL); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 7c719560..976aa680 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -10,7 +10,7 @@ import { css } from 'styled-components'; import * as Y from 'yjs'; import { Box, TextErrors } from '@/components'; -import { useAuthStore } from '@/features/auth'; +import { useAuth } from '@/features/auth'; import { Doc } from '@/features/docs/doc-management'; import { useUploadFile } from '../hook'; @@ -113,7 +113,7 @@ interface BlockNoteEditorProps { } export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { - const { userData } = useAuthStore(); + const { user } = useAuth(); const { setEditor } = useEditorStore(); const { t } = useTranslation(); @@ -126,7 +126,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { const collabName = readOnly ? 'Reader' - : userData?.full_name || userData?.email || t('Anonymous'); + : user?.full_name || user?.email || t('Anonymous'); const editor = useCreateBlockNote( { diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx index 7cc77d8e..c5b57b29 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx @@ -1,16 +1,16 @@ -import { useAuthStore } from '@/features/auth'; +import { useAuth } from '@/features/auth'; import { Access, Role } from '@/features/docs/doc-management'; export const useWhoAmI = (access: Access) => { - const { userData } = useAuthStore(); + const { user } = useAuth(); - const isMyself = userData?.id === access.user.id; + const isMyself = user?.id === access.user.id; const rolesAllowed = access.abilities.set_role_to; const isLastOwner = !rolesAllowed.length && access.role === Role.OWNER && isMyself; - const isOtherOwner = access.role === Role.OWNER && userData?.id && !isMyself; + const isOtherOwner = access.role === Role.OWNER && user?.id && !isMyself; return { isLastOwner, diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx index abe26a91..640a731a 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation'; import { PropsWithChildren } from 'react'; import { Box, Icon, SeparatedSection } from '@/components'; -import { useAuthStore } from '@/features/auth'; +import { useAuth } from '@/features/auth'; import { useCreateDoc } from '@/features/docs/doc-management'; import { DocSearchModal } from '@/features/docs/doc-search'; import { useCmdK } from '@/hook/useCmdK'; @@ -14,7 +14,7 @@ import { useLeftPanelStore } from '../stores'; export const LeftPanelHeader = ({ children }: PropsWithChildren) => { const router = useRouter(); const searchModal = useModal(); - const auth = useAuthStore(); + const { authenticated } = useAuth(); useCmdK(searchModal.open); const { togglePanel } = useLeftPanelStore(); @@ -54,7 +54,7 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { } /> - {auth.authenticated && ( + {authenticated && ( )} diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx index 6587db52..a748ebe8 100644 --- a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { Box, Text, TextErrors } from '@/components'; -import { useAuthStore } from '@/features/auth'; +import { gotoLogin } from '@/features/auth'; import { DocEditor } from '@/features/docs/doc-editor'; import { Doc, @@ -45,7 +45,6 @@ interface DocProps { } const DocPage = ({ id }: DocProps) => { - const { login } = useAuthStore(); const { data: docQuery, isError, @@ -106,7 +105,7 @@ const DocPage = ({ id }: DocProps) => { } if (error.status === 401) { - login(); + gotoLogin(); return null; }