diff --git a/src/frontend/apps/desk/.env.test b/src/frontend/apps/desk/.env.test new file mode 100644 index 0000000..eda49a1 --- /dev/null +++ b/src/frontend/apps/desk/.env.test @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=/api/ diff --git a/src/frontend/apps/desk/jest.config.ts b/src/frontend/apps/desk/jest.config.ts index 9fa449d..a6841d7 100644 --- a/src/frontend/apps/desk/jest.config.ts +++ b/src/frontend/apps/desk/jest.config.ts @@ -9,6 +9,7 @@ const createJestConfig = nextJest({ const config: Config = { coverageProvider: 'v8', testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/jest.setup.ts'], }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/src/frontend/apps/desk/jest.setup.ts b/src/frontend/apps/desk/jest.setup.ts new file mode 100644 index 0000000..3aa7aec --- /dev/null +++ b/src/frontend/apps/desk/jest.setup.ts @@ -0,0 +1,3 @@ +import * as dotenv from 'dotenv'; + +dotenv.config({ path: './.env.test' }); diff --git a/src/frontend/apps/desk/next.config.js b/src/frontend/apps/desk/next.config.js index 90829d9..9355ff6 100644 --- a/src/frontend/apps/desk/next.config.js +++ b/src/frontend/apps/desk/next.config.js @@ -1,14 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - trailingSlash: true, - async rewrites() { - return [ - { - source: '/api/:slug*/', - destination: `${process.env.NEXT_PUBLIC_API_URL}:slug*/`, // Matched parameters can be used in the destination - }, - ]; - }, }; module.exports = nextConfig; diff --git a/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx b/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx new file mode 100644 index 0000000..6c15962 --- /dev/null +++ b/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx @@ -0,0 +1,48 @@ +import fetchMock from 'fetch-mock'; + +import useAuthStore from '@/auth/useAuthStore'; + +import { fetchAPI } from '../fetchApi'; + +describe('fetchAPI', () => { + beforeEach(() => { + process.env.NEXT_PUBLIC_API_URL = 'http://some.api.url/api/v1.0/'; + fetchMock.restore(); + }); + + it('adds correctly the basename', () => { + fetchMock.mock('http://some.api.url/api/v1.0/some/url', 200); + + void fetchAPI('some/url'); + + expect(fetchMock.lastUrl()).toEqual( + 'http://some.api.url/api/v1.0/some/url', + ); + }); + + it('adds the BEARER automatically', () => { + useAuthStore.setState({ token: 'my-token' }); + + fetchMock.mock('http://some.api.url/api/v1.0/some/url', 200); + + void fetchAPI('some/url', { body: 'some body' }); + + expect(fetchMock.lastOptions()).toEqual({ + body: 'some body', + headers: { + Authorization: 'Bearer my-token', + 'Content-Type': 'application/json', + }, + }); + }); + + it('logout if 401 response', async () => { + useAuthStore.setState({ token: 'my-token' }); + + fetchMock.mock('http://some.api.url/api/v1.0/some/url', 401); + + await fetchAPI('some/url'); + + expect(useAuthStore.getState().token).toBeNull(); + }); +}); diff --git a/src/frontend/apps/desk/src/api/fetchApi.ts b/src/frontend/apps/desk/src/api/fetchApi.ts new file mode 100644 index 0000000..1f68820 --- /dev/null +++ b/src/frontend/apps/desk/src/api/fetchApi.ts @@ -0,0 +1,19 @@ +import useAuthStore from '@/auth/useAuthStore'; + +export const fetchAPI = async (input: string, init?: RequestInit) => { + const apiUrl = `${process.env.NEXT_PUBLIC_API_URL}${input}`; + const { token, logout } = useAuthStore.getState(); + + const response = await fetch(apiUrl, { + ...init, + headers: { + ...init?.headers, + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + + response.status === 401 && logout(); + + return response; +}; diff --git a/src/frontend/apps/desk/src/api/index.ts b/src/frontend/apps/desk/src/api/index.ts new file mode 100644 index 0000000..f9f4a38 --- /dev/null +++ b/src/frontend/apps/desk/src/api/index.ts @@ -0,0 +1,2 @@ +export * from './fetchApi'; +export * from './types'; diff --git a/src/frontend/apps/desk/src/types/api.ts b/src/frontend/apps/desk/src/api/types.ts similarity index 100% rename from src/frontend/apps/desk/src/types/api.ts rename to src/frontend/apps/desk/src/api/types.ts diff --git a/src/frontend/apps/desk/src/app/Teams/api/useCreateTeam.tsx b/src/frontend/apps/desk/src/app/Teams/api/useCreateTeam.tsx index 25320ea..3ff7a89 100644 --- a/src/frontend/apps/desk/src/app/Teams/api/useCreateTeam.tsx +++ b/src/frontend/apps/desk/src/app/Teams/api/useCreateTeam.tsx @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import useAuthStore from '@/auth/useAuthStore'; +import { fetchAPI } from '@/api'; import { KEY_LIST_TEAM } from './useTeams'; @@ -13,13 +13,7 @@ export interface CreateTeamResponseError { } export const createTeam = async (name: string) => { - const { token } = useAuthStore.getState(); - - const response = await fetch(`/api/teams/`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, + const response = await fetchAPI(`teams/`, { method: 'POST', body: JSON.stringify({ name, diff --git a/src/frontend/apps/desk/src/app/Teams/api/useTeams.tsx b/src/frontend/apps/desk/src/app/Teams/api/useTeams.tsx index 6afd200..9a107bb 100644 --- a/src/frontend/apps/desk/src/app/Teams/api/useTeams.tsx +++ b/src/frontend/apps/desk/src/app/Teams/api/useTeams.tsx @@ -1,7 +1,6 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query'; -import useAuthStore from '@/auth/useAuthStore'; -import { APIList } from '@/types/api'; +import { APIList, fetchAPI } from '@/api'; interface TeamResponse { id: string; @@ -14,19 +13,11 @@ export interface TeamsResponseError { } export const getTeams = async () => { - const token = useAuthStore.getState().token; - - const response = await fetch(`/api/teams/`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - }); + const response = await fetchAPI(`teams/`); if (!response.ok) { throw new Error(`Couldn't fetch teams: ${response.statusText}`); } - return response.json(); }; diff --git a/src/frontend/apps/desk/src/app/page.tsx b/src/frontend/apps/desk/src/app/page.tsx index 8bfb9e9..a367b0d 100644 --- a/src/frontend/apps/desk/src/app/page.tsx +++ b/src/frontend/apps/desk/src/app/page.tsx @@ -9,11 +9,15 @@ import { Teams } from './Teams'; import styles from './page.module.css'; export default function Home() { - const { initAuth, authenticated } = useAuthStore(); + const { initAuth, authenticated, initialized } = useAuthStore(); useEffect(() => { + if (initialized) { + return; + } + initAuth(); - }, [initAuth]); + }, [initAuth, initialized]); if (!authenticated) { return ; diff --git a/src/frontend/apps/desk/src/auth/useAuthStore.tsx b/src/frontend/apps/desk/src/auth/useAuthStore.tsx index 342624a..f673cff 100644 --- a/src/frontend/apps/desk/src/auth/useAuthStore.tsx +++ b/src/frontend/apps/desk/src/auth/useAuthStore.tsx @@ -3,16 +3,23 @@ import { create } from 'zustand'; import { initKeycloak } from './keycloak'; interface AuthStore { - initialized: boolean; authenticated: boolean; - token: string | null; initAuth: () => void; + initialized: boolean; + logout: () => void; + token: string | null; } -const useAuthStore = create((set) => ({ - initialized: false, +const initialState = { authenticated: false, + initialized: false, token: null, +}; + +const useAuthStore = create((set) => ({ + authenticated: initialState.authenticated, + initialized: initialState.initialized, + token: initialState.token, initAuth: () => set((state) => { @@ -27,6 +34,10 @@ const useAuthStore = create((set) => ({ return {}; }), + + logout: () => { + set(initialState); + }, })); export default useAuthStore; diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index e9b912a..d15c217 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -3194,6 +3194,11 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" +dotenv@16.3.1: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + downshift@8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/downshift/-/downshift-8.2.3.tgz#27106a5d9f408a6f6f9350ca465801d07e52db87"