Skip to content

fe: wip #515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -111,11 +111,9 @@ const RenderCasebegrunnelsePage = ({ data }: Props) => {
</GuidePanel>

<PersonligeOpplysningerSummary
fornavn={isLoadingUser ? user_loader.loading_user : user.navn.fornavn}
etternavn={isLoadingUser ? user_loader.loading_user : user.navn.etternavn}
f_or_d_number={
isLoadingUser ? user_loader.loading_user : user.folkeregisteridentifikator?.identifikasjonsnummer
}
fornavn={isSuccess ? user.navn.fornavn : user_loader.loading_user}
etternavn={isSuccess ? user.navn.etternavn : user_loader.loading_user}
f_or_d_number={isSuccess ? user.folkeregisteridentifikator?.identifikasjonsnummer : user_loader.loading_user}
/>

{isKlage ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
Expand Down Expand Up @@ -63,10 +63,10 @@ const DigitalCaseOppsummeringPage = ({ data }: Props) => {
</Heading>
<BodyLong spacing>{skjema.summary.sections.person.info_from}</BodyLong>
<PersonligeOpplysningerSummary
fornavn={isLoadingUser ? user_loader.loading_user : user.navn.fornavn}
etternavn={isLoadingUser ? user_loader.loading_user : user.navn.etternavn}
fornavn={isSuccess ? user.navn.fornavn : user_loader.loading_user}
etternavn={isSuccess ? user.navn.etternavn : user_loader.loading_user}
f_or_d_number={
isLoadingUser ? user_loader.loading_user : user.folkeregisteridentifikator?.identifikasjonsnummer
isSuccess ? user.folkeregisteridentifikator?.identifikasjonsnummer : user_loader.loading_user
}
/>
</Section>
Expand Down
29 changes: 11 additions & 18 deletions frontend/src/functions/is-api-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
68 changes: 15 additions & 53 deletions frontend/src/hooks/use-user.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
};
5 changes: 3 additions & 2 deletions frontend/src/redux-api/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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');
3 changes: 1 addition & 2 deletions frontend/src/redux-api/server-sent-events.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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;
}
Expand Down
59 changes: 5 additions & 54 deletions frontend/src/redux-api/user/api.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,19 @@
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({
reducerPath: 'userApi',
baseQuery: API_BASE_QUERY,
tagTypes: ['user'],
endpoints: (builder) => ({
getUser: builder.query<IUser, void>({
query: () => '/bruker',
getUser: builder.query<IUser | undefined, void>({
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<OAuthSessionData | null, void>({
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;
11 changes: 2 additions & 9 deletions frontend/src/redux/configure-store.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -31,7 +31,6 @@ const rtkQueryErrorLogger: Middleware = () => (next) => (action) => {

if (action.payload.status === 401) {
userApi.util.invalidateTags(['user']);
oauthApi.util.invalidateTags(['session']);
}
}

Expand All @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/redux/root.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
});
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/routes/create-case/use-case.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'));

Expand All @@ -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,
Expand Down Expand Up @@ -83,9 +83,9 @@ export const useCase = (type: CaseType, innsendingsytelse: Innsendingsytelse): I
dispatch,
innsendingsytelse,
internalSaksnummer,
isAuthenticated,
isDone,
isLoading,
isSuccess,
language,
navigate,
resumeOrCreateCase,
Expand Down
Loading