🛂(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 = {
|
const config: Config = {
|
||||||
coverageProvider: 'v8',
|
coverageProvider: 'v8',
|
||||||
testEnvironment: 'jsdom',
|
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
|
// 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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const 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;
|
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 { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import useAuthStore from '@/auth/useAuthStore';
|
import { fetchAPI } from '@/api';
|
||||||
|
|
||||||
import { KEY_LIST_TEAM } from './useTeams';
|
import { KEY_LIST_TEAM } from './useTeams';
|
||||||
|
|
||||||
@@ -13,13 +13,7 @@ export interface CreateTeamResponseError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const createTeam = async (name: string) => {
|
export const createTeam = async (name: string) => {
|
||||||
const { token } = useAuthStore.getState();
|
const response = await fetchAPI(`teams/`, {
|
||||||
|
|
||||||
const response = await fetch(`/api/teams/`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import useAuthStore from '@/auth/useAuthStore';
|
import { APIList, fetchAPI } from '@/api';
|
||||||
import { APIList } from '@/types/api';
|
|
||||||
|
|
||||||
interface TeamResponse {
|
interface TeamResponse {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -14,19 +13,11 @@ export interface TeamsResponseError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getTeams = async () => {
|
export const getTeams = async () => {
|
||||||
const token = useAuthStore.getState().token;
|
const response = await fetchAPI(`teams/`);
|
||||||
|
|
||||||
const response = await fetch(`/api/teams/`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Couldn't fetch teams: ${response.statusText}`);
|
throw new Error(`Couldn't fetch teams: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ import { Teams } from './Teams';
|
|||||||
import styles from './page.module.css';
|
import styles from './page.module.css';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { initAuth, authenticated } = useAuthStore();
|
const { initAuth, authenticated, initialized } = useAuthStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
initAuth();
|
initAuth();
|
||||||
}, [initAuth]);
|
}, [initAuth, initialized]);
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
|
|||||||
@@ -3,16 +3,23 @@ import { create } from 'zustand';
|
|||||||
import { initKeycloak } from './keycloak';
|
import { initKeycloak } from './keycloak';
|
||||||
|
|
||||||
interface AuthStore {
|
interface AuthStore {
|
||||||
initialized: boolean;
|
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
token: string | null;
|
|
||||||
initAuth: () => void;
|
initAuth: () => void;
|
||||||
|
initialized: boolean;
|
||||||
|
logout: () => void;
|
||||||
|
token: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAuthStore = create<AuthStore>((set) => ({
|
const initialState = {
|
||||||
initialized: false,
|
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
|
initialized: false,
|
||||||
token: null,
|
token: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAuthStore = create<AuthStore>((set) => ({
|
||||||
|
authenticated: initialState.authenticated,
|
||||||
|
initialized: initialState.initialized,
|
||||||
|
token: initialState.token,
|
||||||
|
|
||||||
initAuth: () =>
|
initAuth: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -27,6 +34,10 @@ const useAuthStore = create<AuthStore>((set) => ({
|
|||||||
|
|
||||||
return {};
|
return {};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
set(initialState);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useAuthStore;
|
export default useAuthStore;
|
||||||
|
|||||||
@@ -3194,6 +3194,11 @@ domexception@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
webidl-conversions "^7.0.0"
|
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:
|
downshift@8.2.3:
|
||||||
version "8.2.3"
|
version "8.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/downshift/-/downshift-8.2.3.tgz#27106a5d9f408a6f6f9350ca465801d07e52db87"
|
resolved "https://registry.yarnpkg.com/downshift/-/downshift-8.2.3.tgz#27106a5d9f408a6f6f9350ca465801d07e52db87"
|
||||||
|
|||||||
Reference in New Issue
Block a user