From 2e2dc1be4c180cffb05c2891c796a1893c84912f Mon Sep 17 00:00:00 2001 From: Daniel Eriksson Date: Fri, 31 Jan 2025 10:23:26 +0100 Subject: [PATCH] Remove /oauth2/session usage in order to avoid /bruker spam --- .../begrunnelse/begrunnelse-page.tsx | 10 ++- .../innlogget/summary/oppsummering-page.tsx | 8 +-- frontend/src/functions/is-api-error.ts | 29 +++----- frontend/src/hooks/use-user.ts | 68 ++++--------------- frontend/src/redux-api/common.ts | 5 +- frontend/src/redux-api/server-sent-events.ts | 3 +- frontend/src/redux-api/user/api.ts | 59 ++-------------- frontend/src/redux/configure-store.ts | 11 +-- frontend/src/redux/root.ts | 3 +- frontend/src/routes/create-case/use-case.ts | 10 +-- 10 files changed, 51 insertions(+), 155 deletions(-) diff --git a/frontend/src/components/case/innlogget/begrunnelse/begrunnelse-page.tsx b/frontend/src/components/case/innlogget/begrunnelse/begrunnelse-page.tsx index fb785e71..c8ab57d3 100644 --- a/frontend/src/components/case/innlogget/begrunnelse/begrunnelse-page.tsx +++ b/frontend/src/components/case/innlogget/begrunnelse/begrunnelse-page.tsx @@ -35,7 +35,7 @@ interface Props { const RenderCasebegrunnelsePage = ({ data }: Props) => { const navigate = useNavigate(); const language = useLanguage(); - const { user, isLoadingUser } = useUserRequired(); + const { data: user, isSuccess } = useUserRequired(); const { skjema, user_loader } = useTranslation(); @@ -111,11 +111,9 @@ const RenderCasebegrunnelsePage = ({ data }: Props) => { {isKlage ? ( diff --git a/frontend/src/components/case/innlogget/summary/oppsummering-page.tsx b/frontend/src/components/case/innlogget/summary/oppsummering-page.tsx index d30eae38..73fa80a4 100644 --- a/frontend/src/components/case/innlogget/summary/oppsummering-page.tsx +++ b/frontend/src/components/case/innlogget/summary/oppsummering-page.tsx @@ -30,7 +30,7 @@ interface Props { const DigitalCaseOppsummeringPage = ({ data }: Props) => { const { common, skjema, user_loader, icons } = useTranslation(); - const { user, isLoadingUser } = useUserRequired(); + const { data: user, isSuccess } = useUserRequired(); const validate = useCaseErrors(data.type); const [isValid] = validate(data); const [error, setError] = useState(null); @@ -63,10 +63,10 @@ const DigitalCaseOppsummeringPage = ({ data }: Props) => { {skjema.summary.sections.person.info_from} diff --git a/frontend/src/functions/is-api-error.ts b/frontend/src/functions/is-api-error.ts index 0fe1df94..7eedb8f2 100644 --- a/frontend/src/functions/is-api-error.ts +++ b/frontend/src/functions/is-api-error.ts @@ -5,30 +5,23 @@ interface ApiError { data: ApiErrorData; } -interface ApiErrorData { +export interface ApiErrorData { status: number; detail: string; title: string; } -export const isApiError = (error: unknown): error is ApiError => { - if (isError(error)) { - if ( - error.data !== null && - typeof error.data === 'object' && - 'status' in error.data && - Number.isInteger(error.data.status) && - 'detail' in error.data && - typeof error.data.detail === 'string' && - 'title' in error.data && - typeof error.data.title === 'string' - ) { - return true; - } - } +export const isApiError = (error: unknown): error is ApiError => isError(error) && isApiErrorData(error.data); - return false; -}; +export const isApiErrorData = (data: unknown): data is ApiErrorData => + data !== null && + typeof data === 'object' && + 'status' in data && + Number.isInteger(data.status) && + 'detail' in data && + typeof data.detail === 'string' && + 'title' in data && + typeof data.title === 'string'; export const isError = (error: unknown): error is FetchBaseQueryError => { if (error !== null && typeof error === 'object') { diff --git a/frontend/src/hooks/use-user.ts b/frontend/src/hooks/use-user.ts index fb9fade5..63d5001a 100644 --- a/frontend/src/hooks/use-user.ts +++ b/frontend/src/hooks/use-user.ts @@ -1,7 +1,5 @@ -import { useGetSessionQuery, useGetUserQuery } from '@app/redux-api/user/api'; -import type { IUser } from '@app/redux-api/user/types'; +import { useGetUserQuery } from '@app/redux-api/user/api'; import { login } from '@app/user/login'; -import { type SkipToken, skipToken } from '@reduxjs/toolkit/query'; import { useEffect } from 'react'; interface LoadingAuth { @@ -16,73 +14,37 @@ interface LoadedAuth { type AuthResult = LoadingAuth | LoadedAuth; -export const useIsAuthenticated = (skip?: SkipToken): AuthResult => { - const { data, isSuccess } = useGetSessionQuery(skip, { - refetchOnFocus: true, - refetchOnReconnect: true, - }); +export const useIsAuthenticated = (): AuthResult => { + const { data, isSuccess, isError, isLoading } = useGetUserQuery(); if (isSuccess) { - return { isAuthenticated: data?.session.active ?? false, isLoadingAuth: false }; + return { isAuthenticated: data !== undefined, isLoadingAuth: false }; } - return { isAuthenticated: undefined, isLoadingAuth: true }; -}; - -interface LoadedUser { - user: IUser; - isLoadingUser: false; - isAuthenticated: true; -} - -interface LoadingUser { - user: undefined; - isLoadingUser: true; - isAuthenticated: true; -} - -interface LoadedAnonymous { - user: undefined; - isLoadingUser: false; - isAuthenticated: false; -} - -const LOADED_ANONYMOUS: LoadedAnonymous = { user: undefined, isLoadingUser: false, isAuthenticated: false }; -const LOADING_USER: LoadingUser = { user: undefined, isLoadingUser: true, isAuthenticated: true }; - -type UserResult = LoadedUser | LoadingUser | LoadedAnonymous; - -export const useUser = (): UserResult => { - const { isAuthenticated, isLoadingAuth } = useIsAuthenticated(); - const { data: user, isLoading: isLoadingUser } = useGetUserQuery(isAuthenticated === true ? undefined : skipToken); - - if (isAuthenticated === false) { - return LOADED_ANONYMOUS; + if (isError) { + return { isAuthenticated: false, isLoadingAuth: false }; } - if (user === undefined) { - return isLoadingAuth || isLoadingUser ? LOADING_USER : LOADED_ANONYMOUS; + if (isLoading) { + return { isAuthenticated: undefined, isLoadingAuth: true }; } - return { user, isLoadingUser: false, isAuthenticated: true }; + return { isAuthenticated: undefined, isLoadingAuth: true }; }; /** Only for use in authorized contexts. * If the user is unauthorized, it will redirect to the login page. * It will return a loading state until the user is loaded or redirected. */ -export const useUserRequired = (): LoadingUser | LoadedUser => { - const { user, isLoadingUser, isAuthenticated } = useUser(); +export const useUserRequired = () => { + const user = useGetUserQuery(); + const { data, isSuccess } = user; useEffect(() => { - if (isAuthenticated === false) { + if (isSuccess && data === undefined) { login(); } - }, [isAuthenticated]); - - if (isLoadingUser || !isAuthenticated) { - return LOADING_USER; - } + }, [data, isSuccess]); - return { user, isLoadingUser: false, isAuthenticated: true }; + return user; }; diff --git a/frontend/src/redux-api/common.ts b/frontend/src/redux-api/common.ts index ac21181b..462947c2 100644 --- a/frontend/src/redux-api/common.ts +++ b/frontend/src/redux-api/common.ts @@ -47,7 +47,9 @@ const staggeredBaseQuery = (baseUrl: string) => { retry.fail(result.error); } - if (result.error.status === 401) { + const url = argsIsString ? args : args.url; + + if (result.error.status === 401 && url !== '/bruker') { reduxStore.dispatch(setShow(true)); retry.fail(result.error.data); } else if ( @@ -71,4 +73,3 @@ const staggeredBaseQuery = (baseUrl: string) => { export const API_PATH = '/api/klage-dittnav-api/api'; export const API_BASE_QUERY = staggeredBaseQuery(API_PATH); -export const OAUTH_BASE_QUERY = staggeredBaseQuery('/oauth2'); diff --git a/frontend/src/redux-api/server-sent-events.ts b/frontend/src/redux-api/server-sent-events.ts index b4bff419..73824f74 100644 --- a/frontend/src/redux-api/server-sent-events.ts +++ b/frontend/src/redux-api/server-sent-events.ts @@ -1,6 +1,6 @@ import { AppEventEnum } from '@app/logging/action'; import { apiEvent, appEvent } from '@app/logging/logger'; -import { oauthApi, userApi } from './user/api'; +import { userApi } from './user/api'; export enum ServerSentEventType { JOURNALPOSTID = 'journalpostId', @@ -97,7 +97,6 @@ export class ServerSentEventManager { if (!preflightOK) { // Probably the session timed out. Double check the logged in status. userApi.util.invalidateTags(['user']); - oauthApi.util.invalidateTags(['session']); return; } diff --git a/frontend/src/redux-api/user/api.ts b/frontend/src/redux-api/user/api.ts index 876c4c5d..9ece7785 100644 --- a/frontend/src/redux-api/user/api.ts +++ b/frontend/src/redux-api/user/api.ts @@ -1,6 +1,6 @@ -import { setSessionEndsAt, setTokenExpires } from '@app/logging/logger'; +import { type ApiErrorData, isApiErrorData } from '@app/functions/is-api-error'; import { createApi } from '@reduxjs/toolkit/query/react'; -import { API_BASE_QUERY, OAUTH_BASE_QUERY } from '../common'; +import { API_BASE_QUERY } from '../common'; import type { IUser } from './types'; export const userApi = createApi({ @@ -8,61 +8,12 @@ export const userApi = createApi({ baseQuery: API_BASE_QUERY, tagTypes: ['user'], endpoints: (builder) => ({ - getUser: builder.query({ - query: () => '/bruker', + getUser: builder.query({ + query: () => ({ url: '/bruker', validateStatus: ({ status, ok }) => ok || status === 401 }), + transformResponse: (response: IUser | ApiErrorData) => (isApiErrorData(response) ? undefined : response), providesTags: ['user'], }), }), }); -interface OAuthSessionData { - session: { - created_at: string; - /** DateTime when the session ends. */ - ends_at: string; - timeout_at: string; - /** How long the session has left in seconds. */ - ends_in_seconds: number; - active: boolean; - timeout_in_seconds: number; - }; - tokens: { - /** DateTime when the token expires. */ - expire_at: string; - refreshed_at: string; - expire_in_seconds: number; - next_auto_refresh_in_seconds: number; - refresh_cooldown: boolean; - refresh_cooldown_seconds: number; - }; -} - -export const oauthApi = createApi({ - reducerPath: 'oauthApi', - baseQuery: OAUTH_BASE_QUERY, - tagTypes: ['session'], - endpoints: (builder) => ({ - getSession: builder.query({ - query: () => ({ url: '/session', validateStatus: ({ status, ok }) => ok || status === 401 }), - providesTags: ['session'], - onQueryStarted: async (_, { dispatch, queryFulfilled }) => { - const { data } = await queryFulfilled; - - const hasData = data !== null; - - if (hasData) { - setTokenExpires(data.tokens.expire_at); - setSessionEndsAt(data.session.ends_at); - } - - if (!(hasData && data.session.active)) { - dispatch(userApi.util.updateQueryData('getUser', undefined, () => undefined)); - } - }, - }), - }), -}); - -export const { useGetSessionQuery } = oauthApi; - export const { useGetUserQuery } = userApi; diff --git a/frontend/src/redux/configure-store.ts b/frontend/src/redux/configure-store.ts index 1fd34bf8..2353db33 100644 --- a/frontend/src/redux/configure-store.ts +++ b/frontend/src/redux/configure-store.ts @@ -1,6 +1,6 @@ import { caseApi } from '@app/redux-api/case/api'; import { innsendingsytelserApi } from '@app/redux-api/innsendingsytelser'; -import { oauthApi, userApi } from '@app/redux-api/user/api'; +import { userApi } from '@app/redux-api/user/api'; import { type Middleware, configureStore } from '@reduxjs/toolkit'; import { type TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { type RootState, rootReducer } from './root'; @@ -31,7 +31,6 @@ const rtkQueryErrorLogger: Middleware = () => (next) => (action) => { if (action.payload.status === 401) { userApi.util.invalidateTags(['user']); - oauthApi.util.invalidateTags(['session']); } } @@ -53,13 +52,7 @@ export const reduxStore = configureStore({ 'meta.arg.originalArgs.file', ], }, - }).concat([ - innsendingsytelserApi.middleware, - userApi.middleware, - caseApi.middleware, - oauthApi.middleware, - rtkQueryErrorLogger, - ]), + }).concat([innsendingsytelserApi.middleware, userApi.middleware, caseApi.middleware, rtkQueryErrorLogger]), }); export type AppDispatch = typeof reduxStore.dispatch; diff --git a/frontend/src/redux/root.ts b/frontend/src/redux/root.ts index c430efc4..f09a08ac 100644 --- a/frontend/src/redux/root.ts +++ b/frontend/src/redux/root.ts @@ -1,6 +1,6 @@ import { caseApi } from '@app/redux-api/case/api'; import { innsendingsytelserApi } from '@app/redux-api/innsendingsytelser'; -import { oauthApi, userApi } from '@app/redux-api/user/api'; +import { userApi } from '@app/redux-api/user/api'; import { loggedOutModalSlice } from '@app/redux/logged-out-modal'; import { combineReducers } from 'redux'; import { sessionSlice } from './session/session'; @@ -9,7 +9,6 @@ export const rootReducer = combineReducers({ [innsendingsytelserApi.reducerPath]: innsendingsytelserApi.reducer, [userApi.reducerPath]: userApi.reducer, [caseApi.reducerPath]: caseApi.reducer, - [oauthApi.reducerPath]: oauthApi.reducer, session: sessionSlice.reducer, loggedOutModal: loggedOutModalSlice.reducer, }); diff --git a/frontend/src/routes/create-case/use-case.ts b/frontend/src/routes/create-case/use-case.ts index 8758e541..7a8050e0 100644 --- a/frontend/src/routes/create-case/use-case.ts +++ b/frontend/src/routes/create-case/use-case.ts @@ -1,11 +1,11 @@ import { getQueryValue } from '@app/functions/get-query-value'; import { useSessionCase } from '@app/hooks/use-session-klage'; -import { useUser } from '@app/hooks/use-user'; import type { Innsendingsytelse } from '@app/innsendingsytelser/innsendingsytelser'; import { useLanguage } from '@app/language/use-language'; import { useTranslation } from '@app/language/use-translation'; import { useCreateCaseMutation, useResumeOrCreateCaseMutation } from '@app/redux-api/case/api'; import type { CaseType } from '@app/redux-api/case/types'; +import { useGetUserQuery } from '@app/redux-api/user/api'; import { useAppDispatch } from '@app/redux/configure-store'; import { useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; @@ -21,7 +21,7 @@ export const useCase = (type: CaseType, innsendingsytelse: Innsendingsytelse): I const language = useLanguage(); const { case_loader, error_messages } = useTranslation(); const [query] = useSearchParams(); - const { user, isLoadingUser, isAuthenticated } = useUser(); + const { data: user, isLoading: isLoadingUser, isSuccess } = useGetUserQuery(); const internalSaksnummer = getQueryValue(query.get('saksnummer')); @@ -38,11 +38,11 @@ export const useCase = (type: CaseType, innsendingsytelse: Innsendingsytelse): I const isDone = createHasFailed || createIsSuccess || resumeHasFailed || resumeIsSuccess; useEffect(() => { - if (isLoading || isDone || sessionCaseIsLoading || innsendingsytelse === null) { + if (!isSuccess || isLoading || isDone || sessionCaseIsLoading || innsendingsytelse === null) { return; } - if (isAuthenticated === false) { + if (user === undefined) { handleSessionCase({ type, dispatch, @@ -83,9 +83,9 @@ export const useCase = (type: CaseType, innsendingsytelse: Innsendingsytelse): I dispatch, innsendingsytelse, internalSaksnummer, - isAuthenticated, isDone, isLoading, + isSuccess, language, navigate, resumeOrCreateCase,