From 5491e101b27cf281df9efa7c423df200f1818397 Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Thu, 4 Jun 2026 08:49:48 +0200 Subject: [PATCH 1/2] fix(frontend): login/logout correct handling --- frontend/src/api/auth/api.ts | 10 ++++++---- frontend/src/api/graphqlClient.ts | 6 +++--- frontend/src/api/utils.ts | 3 +++ frontend/src/features/User/api/index.ts | 6 +++++- frontend/src/routes/(public)/login.tsx | 10 ++++++---- frontend/src/routes/_authenticated.tsx | 14 +++++--------- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/frontend/src/api/auth/api.ts b/frontend/src/api/auth/api.ts index 6a4203762..13c846607 100644 --- a/frontend/src/api/auth/api.ts +++ b/frontend/src/api/auth/api.ts @@ -1,6 +1,7 @@ import { restAPI } from '../baseRestApi'; import { invalidateEntity, queryKeys } from '@/api/queryKeys'; import { queryClient } from '@/api/queryClient'; +import { clearTokenFromCookie } from '@/api/utils'; export type Token = string | undefined; @@ -22,8 +23,9 @@ export const AuthRestAPI = restAPI.injectEndpoints({ }), logout: builder.mutation({ queryFn: async () => { - document.cookie = 'token=;max-age=0;path=/'; - queryClient.invalidateQueries() + clearTokenFromCookie() + queryClient.invalidateQueries({ queryKey: queryKeys.user.current }) + // .then(() => queryClient.clear()) return { data: null } }, }), @@ -33,8 +35,8 @@ export const AuthRestAPI = restAPI.injectEndpoints({ headers: { 'Content-Type': 'text/markdown', }, - responseHandler: 'text' - }) + responseHandler: 'text', + }), }), }), }) diff --git a/frontend/src/api/graphqlClient.ts b/frontend/src/api/graphqlClient.ts index 884f1e99d..f55bde602 100644 --- a/frontend/src/api/graphqlClient.ts +++ b/frontend/src/api/graphqlClient.ts @@ -1,6 +1,6 @@ import type { RequestExtendedOptions } from 'graphql-request'; import { GraphQLClient } from 'graphql-request'; -import { getTokenFromCookie } from '@/api/utils'; +import { clearTokenFromCookie, getTokenFromCookie } from '@/api/utils'; /** @@ -10,7 +10,7 @@ const requestMiddleware = async (request: any) => { const token = getTokenFromCookie(); if (!token) { - document.cookie = 'token=;max-age=0;path=/'; + clearTokenFromCookie() throw new Error('Unauthorized'); } @@ -35,7 +35,7 @@ const responseMiddleware = (response: any) => { ); if (hasAuthError) { - document.cookie = 'token=;max-age=0;path=/'; + clearTokenFromCookie() throw new Error('Unauthorized'); } diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts index f2643e96f..a6fd115eb 100644 --- a/frontend/src/api/utils.ts +++ b/frontend/src/api/utils.ts @@ -12,6 +12,9 @@ export function getTokenFromCookie(): Token | undefined { const tokenCookie = document.cookie?.split(';').filter((item) => item.trim().startsWith('token='))[0]; return tokenCookie?.split('=').pop(); } +export function clearTokenFromCookie(): void { + document.cookie = 'token=;max-age=0;path=/'; +} export function prepareHeaders(headers: Headers) { // Set Authorization diff --git a/frontend/src/features/User/api/index.ts b/frontend/src/features/User/api/index.ts index ba3e880d0..e6b8d4a61 100644 --- a/frontend/src/features/User/api/index.ts +++ b/frontend/src/features/User/api/index.ts @@ -20,7 +20,11 @@ import { cleanGqlList } from '@/api/utils'; export const currentQuery = queryOptions({ queryKey: queryKeys.user.current, queryFn: () => graphqlClient.request(GetCurrentUserDocument, {}) - .then(data => data.currentUser!), + .then(data => data.currentUser!) + .catch(e => { + if (e.message === 'Unauthorized') return null + throw e + }), }) export const allQuery = queryOptions({ diff --git a/frontend/src/routes/(public)/login.tsx b/frontend/src/routes/(public)/login.tsx index 0b67701be..02ebbb4d4 100644 --- a/frontend/src/routes/(public)/login.tsx +++ b/frontend/src/routes/(public)/login.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { createFileRoute, useNavigate } from '@tanstack/react-router' import { IonButton, IonSpinner } from '@ionic/react'; +import { useQuery } from '@tanstack/react-query'; import { Link, useToast } from '@/components/ui'; import { Input } from '@/components/form'; @@ -8,6 +9,7 @@ import { getErrorMessage } from '@/service/function'; import { useLogin } from '@/api'; import { NON_FILTERED_KEY_DOWN_EVENT, useEvent } from '@/features/UX'; +import { User } from '@/features' import styles from './public.module.scss'; @@ -25,6 +27,7 @@ const Login: React.FC = () => { const to = useMemo(() => search?.redirect || '/annotation-campaign', [ search ]); const [ login, { isLoading, error: loginError } ] = useLogin(); const toast = useToast() + const { refetch: refetchUser } = useQuery(User.API.currentQuery) useEffect(() => { return () => { @@ -43,11 +46,10 @@ const Login: React.FC = () => { if (!username || !password) return; await login({ username, password }).unwrap() - .then(() => { - navigate({ to, replace: true }) - }) + .then(() => refetchUser()) + .then(() => navigate({ to, replace: true })) .catch(error => setErrors({ global: getErrorMessage(error) })); - }, [ setErrors, username, password, navigate, to, login ]) + }, [ setErrors, username, password, navigate, to, login, refetchUser ]) const onKbdEvent = useCallback((event: KeyboardEvent) => { switch (event.code) { diff --git a/frontend/src/routes/_authenticated.tsx b/frontend/src/routes/_authenticated.tsx index bfc37aff6..b6e31c5ad 100644 --- a/frontend/src/routes/_authenticated.tsx +++ b/frontend/src/routes/_authenticated.tsx @@ -11,7 +11,7 @@ import { queryKeys } from '@/api/queryKeys'; import { User } from '@/features'; const Component: React.FC = () => { - const { status, error, isFetching } = useQuery(User.API.currentQuery) + const { status, error, isFetching, data: user } = useQuery(User.API.currentQuery) const navigate = useNavigate(); const router = useRouter(); @@ -29,19 +29,15 @@ const Component: React.FC = () => { }, [ router, toast, navigate, error ]) useEffect(() => { - if (!isFetching && status === 'error') handleNotConnected() - }, [ status ]); + if (!isFetching && (status === 'error' || !user)) handleNotConnected() + }, [ status, user ]); return } export const Route = createFileRoute('/_authenticated')({ loader: async () => { - try { - const user = await queryClient.ensureQueryData(User.API.currentQuery) - if (user) return { user } - } catch (e) { - if (![ 'Unauthorized', 'Authentication failed' ].includes((e as Error).message)) throw e - } + const user = await queryClient.ensureQueryData(User.API.currentQuery) + if (user) return { user } throw redirect({ to: '/login', search: { redirect: location.pathname.replace('/app', '') }, From e90e3569be15939e9ba822a461035ae19fb1f775 Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Thu, 4 Jun 2026 09:09:09 +0200 Subject: [PATCH 2/2] fix(frontend): update tabs and page content on phase creation/ending --- .../src/features/AnnotationPhase/PhaseTab.tsx | 42 +++++++++---------- .../_detailLayout/phase.$phaseType.tsx | 16 ++++--- .../annotation-campaign/index.tsx | 5 ++- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/frontend/src/features/AnnotationPhase/PhaseTab.tsx b/frontend/src/features/AnnotationPhase/PhaseTab.tsx index 5b2ff27f1..86f9ef6fd 100644 --- a/frontend/src/features/AnnotationPhase/PhaseTab.tsx +++ b/frontend/src/features/AnnotationPhase/PhaseTab.tsx @@ -4,16 +4,18 @@ import { addOutline, closeOutline } from 'ionicons/icons/index.js'; import { Button, Tab, useAlert, useModal } from '@/components/ui'; import { AnnotationPhaseType } from '@/api'; import { AnnotationPhaseCreateAnnotationModal, AnnotationPhaseCreateVerificationModal } from './PhaseCreateModal' -import { useLoaderData, useParams } from '@tanstack/react-router'; -import { useMutation } from '@tanstack/react-query'; +import { useParams } from '@tanstack/react-router'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { endMutation } from './api' import { queryClient } from '@/api/queryClient'; import { queryKeys } from '@/api/queryKeys'; +import { AnnotationCampaign } from '@/features'; export const AnnotationPhaseTab: React.FC<{ phaseType: AnnotationPhaseType }> = ({ phaseType: phaseType }) => { const { phaseType: currentPhaseType } = useParams({ strict: false }); - const { campaign, phases } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID' }) - const phase = useMemo(() => phases?.find(p => p.phase === phaseType), [ phases, phaseType ]) + const { campaignID } = useParams({ from: '/_authenticated/annotation-campaign/$campaignID' }) + const { data, isFetching } = useQuery(AnnotationCampaign.API.byIdQuery({ id: campaignID })) + const phase = useMemo(() => data?.phases?.find(p => p.phase === phaseType), [ data, phaseType ]) const alert = useAlert(); const verificationModal = useModal(AnnotationPhaseCreateVerificationModal); @@ -27,8 +29,8 @@ export const AnnotationPhaseTab: React.FC<{ phaseType: AnnotationPhaseType }> = annotationModal.toggle() break; case AnnotationPhaseType.Verification: - if (!phases) return; - if (phases.find(p => p.phase === 'Annotation')) return verificationModal.toggle() + if (!data) return; + if (data.phases.find(p => p.phase === 'Annotation')) return verificationModal.toggle() else { return alert.showAlert({ type: 'Warning', @@ -42,18 +44,13 @@ export const AnnotationPhaseTab: React.FC<{ phaseType: AnnotationPhaseType }> = }) } } - }, [ phases, annotationModal, verificationModal, alert, phaseType ]) + }, [ data, annotationModal, verificationModal, alert, phaseType ]) const onSuccess = useCallback(() => { - queryClient.invalidateQueries({ queryKey: queryKeys.campaign.byId({ id: campaign.id }) }) + queryClient.invalidateQueries({ queryKey: queryKeys.campaign.byId({ id: campaignID }) }) queryClient.invalidateQueries({ queryKey: queryKeys.campaign.base }) - queryClient.invalidateQueries({ - queryKey: queryKeys.phase.get({ - campaignID: campaign.id, - phase: phaseType, - }), - }) - }, [ campaign, phaseType ]) + queryClient.invalidateQueries({ queryKey: queryKeys.phase.get({ campaignID, phase: phaseType }) }) + }, [ campaignID, phaseType ]) const { mutate: endPhase } = useMutation({ ...endMutation, onSuccess, @@ -71,20 +68,23 @@ export const AnnotationPhaseTab: React.FC<{ phaseType: AnnotationPhaseType }> = } ], }); } else endPhase({ id: phase.id }) - }, [ endPhase, phase, campaign, alert ]); + }, [ endPhase, phase, alert ]); - if (phase) + if (data?.campaign && phase) return + disabled={isFetching} + params={ { campaignID, phaseType } } active={ currentPhaseType === phaseType }> { phaseType } - { campaign.isEditable && campaign.isUserAllowedToManage && currentPhaseType === phaseType && phase?.isOpen && + { data.campaign.isEditable && data.campaign.isUserAllowedToManage && currentPhaseType === phaseType && phase?.isOpen && } - if (!campaign.isEditable || !campaign.isUserAllowedToManage) return + if (!data?.campaign?.isEditable || !data?.campaign?.isUserAllowedToManage) return return - diff --git a/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase.$phaseType.tsx b/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase.$phaseType.tsx index ee19e4d18..24b6eb9ab 100644 --- a/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase.$phaseType.tsx +++ b/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase.$phaseType.tsx @@ -14,10 +14,16 @@ import styles from './phase.$phaseType.module.scss'; import { queryClient } from '@/api/queryClient'; import { AnnotationPhase, AnnotationSpectrogram, User } from '@/features'; import { IonNote, IonSpinner } from '@ionic/react'; +import { useQuery } from '@tanstack/react-query'; const AnnotationCampaignPhaseDetail: React.FC = () => { const { campaign, phases } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID' }) - const { phase, spectrograms, spectrogramsPageCount } = Route.useLoaderData() + const { spectrograms, spectrogramsPageCount } = Route.useLoaderData() + const { campaignID, phaseType } = Route.useParams() + const { data: phase } = useQuery(AnnotationPhase.API.getQuery({ + campaignID, + phase: phaseType, + })) const search = Route.useSearch(); const routeParams = Route.useParams() @@ -57,7 +63,7 @@ const AnnotationCampaignPhaseDetail: React.FC = () => { - { phase.phase === 'Verification' && !phase.hasAnnotations && phases.find(p => p.phase === AnnotationPhaseType.Verification) && + { phase?.phase === 'Verification' && !phase?.hasAnnotations && phases.find(p => p.phase === AnnotationPhaseType.Verification) && }/> } @@ -74,9 +80,9 @@ const AnnotationCampaignPhaseDetail: React.FC = () => { - Annotations{ phase.phase === 'Verification' &&
to check
} + Annotations{ phase?.phase === 'Verification' &&
to check
} - { phase.phase === 'Verification' && Validated
annotations } + { phase?.phase === 'Verification' && Validated
annotations } @@ -136,7 +142,7 @@ export const Route = createFileRoute('/_authenticated/annotation-campaign/$campa queryClient.ensureQueryData(AnnotationSpectrogram.API.allQuery({ campaignID, phaseType, - annotatorID: user.id, + annotatorID: user!.id, limit: PAGE_SIZE, offset: PAGE_SIZE * ((deps.page ?? 1) - 1), ...deps, diff --git a/frontend/src/routes/_authenticated/annotation-campaign/index.tsx b/frontend/src/routes/_authenticated/annotation-campaign/index.tsx index a3e806c9d..24036a60e 100644 --- a/frontend/src/routes/_authenticated/annotation-campaign/index.tsx +++ b/frontend/src/routes/_authenticated/annotation-campaign/index.tsx @@ -10,11 +10,14 @@ import { } from '@/features/AnnotationCampaign'; import { queryClient } from '@/api/queryClient'; import { AnnotationCampaign } from '@/features'; +import { useQuery } from '@tanstack/react-query'; const AnnotationCampaignList: React.FC = () => { const navigate = useNavigate(); const { user } = useLoaderData({ from: '/_authenticated' }) - const campaigns = Route.useLoaderData() + const params = Route.useParams() + const search = Route.useSearch() + const { data: campaigns } = useQuery(AnnotationCampaign.API.allQuery({ ...search, ...params })) const init = useCallback(() => { navigate({