♻️(frontend) frontend environment free

Until now, the front had to know at build time
the url of the backend and the webrtc server
to be able to communicate with them.
It is not optimal because it means that we need
multiple docker image (1 per environment) to have
the app working, it is not very flexible.

This commit will make the frontend "environment free"
by determining these urls at runtime.
This commit is contained in:
Anthony LC
2024-06-03 12:53:23 +02:00
committed by Anthony LC
parent 4636c611c6
commit c7d1312f89
13 changed files with 58 additions and 52 deletions

View File

@@ -1 +1 @@
NEXT_PUBLIC_API_URL=http://localhost:8071/api/v1.0/
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071

View File

@@ -1 +0,0 @@
NEXT_PUBLIC_API_URL=https://desk-staging.beta.numerique.gouv.fr/api/v1.0/

View File

@@ -1 +1 @@
NEXT_PUBLIC_API_URL=/api/
NEXT_PUBLIC_API_ORIGIN=http://test.jest

View File

@@ -5,22 +5,19 @@ import { useAuthStore } from '@/core/auth';
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);
fetchMock.mock('http://test.jest/api/v1.0/some/url', 200);
void fetchAPI('some/url');
expect(fetchMock.lastUrl()).toEqual(
'http://some.api.url/api/v1.0/some/url',
);
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v1.0/some/url');
});
it('adds the credentials automatically', () => {
fetchMock.mock('http://some.api.url/api/v1.0/some/url', 200);
fetchMock.mock('http://test.jest/api/v1.0/some/url', 200);
void fetchAPI('some/url', { body: 'some body' });
@@ -39,7 +36,7 @@ describe('fetchAPI', () => {
.spyOn(useAuthStore.getState(), 'logout')
.mockImplementation(logoutMock);
fetchMock.mock('http://some.api.url/api/v1.0/some/url', 401);
fetchMock.mock('http://test.jest/api/v1.0/some/url', 401);
await fetchAPI('some/url');
expect(logoutMock).toHaveBeenCalled();

View File

@@ -0,0 +1,7 @@
export const baseApiUrl = (apiVersion: string = '1.0') => {
const origin =
process.env.NEXT_PUBLIC_API_ORIGIN ||
(typeof window !== 'undefined' ? window.location.origin : '');
return `${origin}/api/v${apiVersion}/`;
};

View File

@@ -1,5 +1,7 @@
import { useAuthStore } from '@/core/auth';
import { baseApiUrl } from './conf';
/**
* Retrieves the CSRF token from the document's cookies.
*
@@ -13,9 +15,12 @@ function getCSRFToken() {
.pop();
}
export const fetchAPI = async (input: string, init?: RequestInit) => {
const apiUrl = `${process.env.NEXT_PUBLIC_API_URL}${input}`;
const { logout } = useAuthStore.getState();
export const fetchAPI = async (
input: string,
init?: RequestInit,
apiVersion = '1.0',
) => {
const apiUrl = `${baseApiUrl(apiVersion)}${input}`;
const csrfToken = getCSRFToken();
@@ -30,6 +35,7 @@ export const fetchAPI = async (input: string, init?: RequestInit) => {
});
if (response.status === 401) {
const { logout } = useAuthStore.getState();
logout();
}

View File

@@ -1,4 +1,5 @@
export * from './APIError';
export * from './conf';
export * from './fetchApi';
export * from './types';
export * from './utils';

View File

@@ -1,12 +1,8 @@
import { create } from 'zustand';
import { User, getMe } from './api';
import { baseApiUrl } from '@/api';
export const login = () => {
window.location.replace(
new URL('authenticate/', process.env.NEXT_PUBLIC_API_URL).href,
);
};
import { User, getMe } from './api';
interface AuthStore {
authenticated: boolean;
@@ -30,12 +26,10 @@ export const useAuthStore = create<AuthStore>((set) => ({
set({ authenticated: true, userData: data });
})
.catch(() => {
login();
window.location.replace(new URL('authenticate/', baseApiUrl()).href);
});
},
logout: () => {
window.location.replace(
new URL('logout/', process.env.NEXT_PUBLIC_API_URL).href,
);
window.location.replace(new URL('logout/', baseApiUrl()).href);
},
}));

View File

@@ -19,6 +19,6 @@ declare module '*.svg?url' {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_URL?: string;
NEXT_PUBLIC_API_ORIGIN?: string;
}
}

View File

@@ -20,7 +20,7 @@ describe('MemberGrid', () => {
});
it('renders with no member to display', async () => {
fetchMock.mock(`/api/teams/123456/accesses/?page=1`, {
fetchMock.mock(`end:/teams/123456/accesses/?page=1`, {
count: 0,
results: [],
});
@@ -76,7 +76,7 @@ describe('MemberGrid', () => {
},
];
fetchMock.mock(`/api/teams/123456/accesses/?page=1`, {
fetchMock.mock(`end:/teams/123456/accesses/?page=1`, {
count: 3,
results: accesses,
});
@@ -99,7 +99,8 @@ describe('MemberGrid', () => {
});
it('checks the pagination', async () => {
fetchMock.get(`begin:/api/teams/123456/accesses/?page=`, {
const regexp = new RegExp(/.*\/teams\/123456\/accesses\/\?page=.*/);
fetchMock.get(regexp, {
count: 40,
results: Array.from({ length: 20 }, (_, i) => ({
id: i,
@@ -119,7 +120,7 @@ describe('MemberGrid', () => {
expect(screen.getByRole('status')).toBeInTheDocument();
expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=1');
expect(fetchMock.lastUrl()).toContain('/teams/123456/accesses/?page=1');
expect(
await screen.findByLabelText('You are currently on page 1'),
@@ -131,7 +132,7 @@ describe('MemberGrid', () => {
await screen.findByLabelText('You are currently on page 2'),
).toBeInTheDocument();
expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=2');
expect(fetchMock.lastUrl()).toContain('/teams/123456/accesses/?page=2');
});
[
@@ -149,7 +150,8 @@ describe('MemberGrid', () => {
},
].forEach(({ role, expected }) => {
it(`checks action button when ${role}`, async () => {
fetchMock.get(`begin:/api/teams/123456/accesses/?page=`, {
const regexp = new RegExp(/.*\/teams\/123456\/accesses\/\?page=.*/);
fetchMock.get(regexp, {
count: 1,
results: [
{
@@ -190,7 +192,7 @@ describe('MemberGrid', () => {
});
it('controls the render when api error', async () => {
fetchMock.mock(`/api/teams/123456/accesses/?page=1`, {
fetchMock.mock(`end:/teams/123456/accesses/?page=1`, {
status: 500,
body: {
cause: 'All broken :(',
@@ -207,7 +209,7 @@ describe('MemberGrid', () => {
});
it('cannot add members when current role is member', () => {
fetchMock.get(`/api/teams/123456/accesses/?page=1`, 200);
fetchMock.get(`end:/teams/123456/accesses/?page=1`, 200);
render(<MemberGrid team={team} currentRole={Role.MEMBER} />, {
wrapper: AppWrapper,
@@ -261,17 +263,17 @@ describe('MemberGrid', () => {
);
const reversedMockedData = [...sortedMockedData].reverse();
fetchMock.get(`/api/teams/123456/accesses/?page=1`, {
fetchMock.get(`end:/teams/123456/accesses/?page=1`, {
count: 3,
results: mockedData,
});
fetchMock.get(`/api/teams/123456/accesses/?page=1&ordering=${ordering}`, {
fetchMock.get(`end:/teams/123456/accesses/?page=1&ordering=${ordering}`, {
count: 3,
results: sortedMockedData,
});
fetchMock.get(`/api/teams/123456/accesses/?page=1&ordering=-${ordering}`, {
fetchMock.get(`end:/teams/123456/accesses/?page=1&ordering=-${ordering}`, {
count: 3,
results: reversedMockedData,
});
@@ -282,7 +284,7 @@ describe('MemberGrid', () => {
expect(screen.getByRole('status')).toBeInTheDocument();
expect(fetchMock.lastUrl()).toBe(`/api/teams/123456/accesses/?page=1`);
expect(fetchMock.lastUrl()).toContain(`/teams/123456/accesses/?page=1`);
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
@@ -298,8 +300,8 @@ describe('MemberGrid', () => {
await userEvent.click(screen.getByText(header_name));
expect(fetchMock.lastUrl()).toBe(
`/api/teams/123456/accesses/?page=1&ordering=${ordering}`,
expect(fetchMock.lastUrl()).toContain(
`/teams/123456/accesses/?page=1&ordering=${ordering}`,
);
await waitFor(() => {
@@ -315,8 +317,8 @@ describe('MemberGrid', () => {
await userEvent.click(screen.getByText(header_name));
expect(fetchMock.lastUrl()).toBe(
`/api/teams/123456/accesses/?page=1&ordering=-${ordering}`,
expect(fetchMock.lastUrl()).toContain(
`/teams/123456/accesses/?page=1&ordering=-${ordering}`,
);
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
@@ -331,7 +333,7 @@ describe('MemberGrid', () => {
await userEvent.click(screen.getByText(header_name));
expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=1');
expect(fetchMock.lastUrl()).toContain('/teams/123456/accesses/?page=1');
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();

View File

@@ -66,7 +66,7 @@ describe('ModalRole', () => {
});
it('updates the role successfully', async () => {
fetchMock.patchOnce(`/api/teams/123/accesses/789/`, {
fetchMock.mock(`end:/teams/123/accesses/789/`, {
status: 200,
ok: true,
});
@@ -110,13 +110,13 @@ describe('ModalRole', () => {
);
});
expect(fetchMock.lastUrl()).toBe(`/api/teams/123/accesses/789/`);
expect(fetchMock.lastUrl()).toContain(`/teams/123/accesses/789/`);
expect(onClose).toHaveBeenCalled();
});
it('fails to update the role', async () => {
fetchMock.patchOnce(`/api/teams/123/accesses/789/`, {
fetchMock.patchOnce(`end:/teams/123/accesses/789/`, {
status: 500,
body: {
detail: 'The server is totally broken',

View File

@@ -23,7 +23,7 @@ describe('PanelTeams', () => {
});
it('renders with no team to display', async () => {
fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, {
fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, {
count: 0,
results: [],
});
@@ -40,7 +40,7 @@ describe('PanelTeams', () => {
});
it('renders an empty team', async () => {
fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, {
fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, {
count: 1,
results: [
{
@@ -61,7 +61,7 @@ describe('PanelTeams', () => {
});
it('renders a team with only 1 member', async () => {
fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, {
fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, {
count: 1,
results: [
{
@@ -87,7 +87,7 @@ describe('PanelTeams', () => {
});
it('renders a non-empty team', async () => {
fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, {
fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, {
count: 1,
results: [
{
@@ -115,7 +115,7 @@ describe('PanelTeams', () => {
});
it('renders the error', async () => {
fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, {
fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, {
status: 500,
});
@@ -131,7 +131,7 @@ describe('PanelTeams', () => {
});
it('renders with team panel open', async () => {
fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, {
fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, {
count: 1,
results: [],
});
@@ -146,7 +146,7 @@ describe('PanelTeams', () => {
});
it('closes and opens the team panel', async () => {
fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, {
fetchMock.get(`end:/teams/?page=1&ordering=-created_at`, {
count: 1,
results: [],
});

View File

@@ -60,7 +60,7 @@ backend:
frontend:
envVars:
PORT: 8080
NEXT_PUBLIC_API_URL: https://desk.127.0.0.1.nip.io/api/v1.0/
NEXT_PUBLIC_API_ORIGIN: https://desk.127.0.0.1.nip.io
replicas: 1
command: