🛂(app-desk) create fetchAPI
Create a fetch wrapper for the API calls, it will handle: - add correct basename on the api request - add Bearer automatically on the api request - logout automatically on 401 request
This commit is contained in:
1
src/frontend/apps/desk/.env.test
Normal file
1
src/frontend/apps/desk/.env.test
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_URL=/api/
|
||||
@@ -9,6 +9,7 @@ const createJestConfig = nextJest({
|
||||
const config: Config = {
|
||||
coverageProvider: 'v8',
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
};
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
|
||||
3
src/frontend/apps/desk/jest.setup.ts
Normal file
3
src/frontend/apps/desk/jest.setup.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: './.env.test' });
|
||||
@@ -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;
|
||||
|
||||
48
src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx
Normal file
48
src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
19
src/frontend/apps/desk/src/api/fetchApi.ts
Normal file
19
src/frontend/apps/desk/src/api/fetchApi.ts
Normal file
@@ -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;
|
||||
};
|
||||
2
src/frontend/apps/desk/src/api/index.ts
Normal file
2
src/frontend/apps/desk/src/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './fetchApi';
|
||||
export * from './types';
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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 <Loader />;
|
||||
|
||||
@@ -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<AuthStore>((set) => ({
|
||||
initialized: false,
|
||||
const initialState = {
|
||||
authenticated: false,
|
||||
initialized: false,
|
||||
token: null,
|
||||
};
|
||||
|
||||
const useAuthStore = create<AuthStore>((set) => ({
|
||||
authenticated: initialState.authenticated,
|
||||
initialized: initialState.initialized,
|
||||
token: initialState.token,
|
||||
|
||||
initAuth: () =>
|
||||
set((state) => {
|
||||
@@ -27,6 +34,10 @@ const useAuthStore = create<AuthStore>((set) => ({
|
||||
|
||||
return {};
|
||||
}),
|
||||
|
||||
logout: () => {
|
||||
set(initialState);
|
||||
},
|
||||
}));
|
||||
|
||||
export default useAuthStore;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user