♻️(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:
@@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_API_URL=http://localhost:8071/api/v1.0/
|
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
NEXT_PUBLIC_API_URL=https://desk-staging.beta.numerique.gouv.fr/api/v1.0/
|
|
||||||
@@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_API_URL=/api/
|
NEXT_PUBLIC_API_ORIGIN=http://test.jest
|
||||||
|
|||||||
@@ -5,22 +5,19 @@ import { useAuthStore } from '@/core/auth';
|
|||||||
|
|
||||||
describe('fetchAPI', () => {
|
describe('fetchAPI', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.NEXT_PUBLIC_API_URL = 'http://some.api.url/api/v1.0/';
|
|
||||||
fetchMock.restore();
|
fetchMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds correctly the basename', () => {
|
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');
|
void fetchAPI('some/url');
|
||||||
|
|
||||||
expect(fetchMock.lastUrl()).toEqual(
|
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v1.0/some/url');
|
||||||
'http://some.api.url/api/v1.0/some/url',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds the credentials automatically', () => {
|
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' });
|
void fetchAPI('some/url', { body: 'some body' });
|
||||||
|
|
||||||
@@ -39,7 +36,7 @@ describe('fetchAPI', () => {
|
|||||||
.spyOn(useAuthStore.getState(), 'logout')
|
.spyOn(useAuthStore.getState(), 'logout')
|
||||||
.mockImplementation(logoutMock);
|
.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');
|
await fetchAPI('some/url');
|
||||||
|
|
||||||
expect(logoutMock).toHaveBeenCalled();
|
expect(logoutMock).toHaveBeenCalled();
|
||||||
|
|||||||
7
src/frontend/apps/desk/src/api/conf.ts
Normal file
7
src/frontend/apps/desk/src/api/conf.ts
Normal 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}/`;
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useAuthStore } from '@/core/auth';
|
import { useAuthStore } from '@/core/auth';
|
||||||
|
|
||||||
|
import { baseApiUrl } from './conf';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the CSRF token from the document's cookies.
|
* Retrieves the CSRF token from the document's cookies.
|
||||||
*
|
*
|
||||||
@@ -13,9 +15,12 @@ function getCSRFToken() {
|
|||||||
.pop();
|
.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchAPI = async (input: string, init?: RequestInit) => {
|
export const fetchAPI = async (
|
||||||
const apiUrl = `${process.env.NEXT_PUBLIC_API_URL}${input}`;
|
input: string,
|
||||||
const { logout } = useAuthStore.getState();
|
init?: RequestInit,
|
||||||
|
apiVersion = '1.0',
|
||||||
|
) => {
|
||||||
|
const apiUrl = `${baseApiUrl(apiVersion)}${input}`;
|
||||||
|
|
||||||
const csrfToken = getCSRFToken();
|
const csrfToken = getCSRFToken();
|
||||||
|
|
||||||
@@ -30,6 +35,7 @@ export const fetchAPI = async (input: string, init?: RequestInit) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
|
const { logout } = useAuthStore.getState();
|
||||||
logout();
|
logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './APIError';
|
export * from './APIError';
|
||||||
|
export * from './conf';
|
||||||
export * from './fetchApi';
|
export * from './fetchApi';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { User, getMe } from './api';
|
import { baseApiUrl } from '@/api';
|
||||||
|
|
||||||
export const login = () => {
|
import { User, getMe } from './api';
|
||||||
window.location.replace(
|
|
||||||
new URL('authenticate/', process.env.NEXT_PUBLIC_API_URL).href,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AuthStore {
|
interface AuthStore {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
@@ -30,12 +26,10 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
|||||||
set({ authenticated: true, userData: data });
|
set({ authenticated: true, userData: data });
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
login();
|
window.location.replace(new URL('authenticate/', baseApiUrl()).href);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
window.location.replace(
|
window.location.replace(new URL('logout/', baseApiUrl()).href);
|
||||||
new URL('logout/', process.env.NEXT_PUBLIC_API_URL).href,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
2
src/frontend/apps/desk/src/custom-next.d.ts
vendored
2
src/frontend/apps/desk/src/custom-next.d.ts
vendored
@@ -19,6 +19,6 @@ declare module '*.svg?url' {
|
|||||||
|
|
||||||
namespace NodeJS {
|
namespace NodeJS {
|
||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
NEXT_PUBLIC_API_URL?: string;
|
NEXT_PUBLIC_API_ORIGIN?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('MemberGrid', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders with no member to display', async () => {
|
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,
|
count: 0,
|
||||||
results: [],
|
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,
|
count: 3,
|
||||||
results: accesses,
|
results: accesses,
|
||||||
});
|
});
|
||||||
@@ -99,7 +99,8 @@ describe('MemberGrid', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('checks the pagination', async () => {
|
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,
|
count: 40,
|
||||||
results: Array.from({ length: 20 }, (_, i) => ({
|
results: Array.from({ length: 20 }, (_, i) => ({
|
||||||
id: i,
|
id: i,
|
||||||
@@ -119,7 +120,7 @@ describe('MemberGrid', () => {
|
|||||||
|
|
||||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
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(
|
expect(
|
||||||
await screen.findByLabelText('You are currently on page 1'),
|
await screen.findByLabelText('You are currently on page 1'),
|
||||||
@@ -131,7 +132,7 @@ describe('MemberGrid', () => {
|
|||||||
await screen.findByLabelText('You are currently on page 2'),
|
await screen.findByLabelText('You are currently on page 2'),
|
||||||
).toBeInTheDocument();
|
).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 }) => {
|
].forEach(({ role, expected }) => {
|
||||||
it(`checks action button when ${role}`, async () => {
|
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,
|
count: 1,
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
@@ -190,7 +192,7 @@ describe('MemberGrid', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('controls the render when api error', async () => {
|
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,
|
status: 500,
|
||||||
body: {
|
body: {
|
||||||
cause: 'All broken :(',
|
cause: 'All broken :(',
|
||||||
@@ -207,7 +209,7 @@ describe('MemberGrid', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('cannot add members when current role is member', () => {
|
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} />, {
|
render(<MemberGrid team={team} currentRole={Role.MEMBER} />, {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
@@ -261,17 +263,17 @@ describe('MemberGrid', () => {
|
|||||||
);
|
);
|
||||||
const reversedMockedData = [...sortedMockedData].reverse();
|
const reversedMockedData = [...sortedMockedData].reverse();
|
||||||
|
|
||||||
fetchMock.get(`/api/teams/123456/accesses/?page=1`, {
|
fetchMock.get(`end:/teams/123456/accesses/?page=1`, {
|
||||||
count: 3,
|
count: 3,
|
||||||
results: mockedData,
|
results: mockedData,
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.get(`/api/teams/123456/accesses/?page=1&ordering=${ordering}`, {
|
fetchMock.get(`end:/teams/123456/accesses/?page=1&ordering=${ordering}`, {
|
||||||
count: 3,
|
count: 3,
|
||||||
results: sortedMockedData,
|
results: sortedMockedData,
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.get(`/api/teams/123456/accesses/?page=1&ordering=-${ordering}`, {
|
fetchMock.get(`end:/teams/123456/accesses/?page=1&ordering=-${ordering}`, {
|
||||||
count: 3,
|
count: 3,
|
||||||
results: reversedMockedData,
|
results: reversedMockedData,
|
||||||
});
|
});
|
||||||
@@ -282,7 +284,7 @@ describe('MemberGrid', () => {
|
|||||||
|
|
||||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
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(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||||
@@ -298,8 +300,8 @@ describe('MemberGrid', () => {
|
|||||||
|
|
||||||
await userEvent.click(screen.getByText(header_name));
|
await userEvent.click(screen.getByText(header_name));
|
||||||
|
|
||||||
expect(fetchMock.lastUrl()).toBe(
|
expect(fetchMock.lastUrl()).toContain(
|
||||||
`/api/teams/123456/accesses/?page=1&ordering=${ordering}`,
|
`/teams/123456/accesses/?page=1&ordering=${ordering}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -315,8 +317,8 @@ describe('MemberGrid', () => {
|
|||||||
|
|
||||||
await userEvent.click(screen.getByText(header_name));
|
await userEvent.click(screen.getByText(header_name));
|
||||||
|
|
||||||
expect(fetchMock.lastUrl()).toBe(
|
expect(fetchMock.lastUrl()).toContain(
|
||||||
`/api/teams/123456/accesses/?page=1&ordering=-${ordering}`,
|
`/teams/123456/accesses/?page=1&ordering=-${ordering}`,
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||||
@@ -331,7 +333,7 @@ describe('MemberGrid', () => {
|
|||||||
|
|
||||||
await userEvent.click(screen.getByText(header_name));
|
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(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe('ModalRole', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('updates the role successfully', async () => {
|
it('updates the role successfully', async () => {
|
||||||
fetchMock.patchOnce(`/api/teams/123/accesses/789/`, {
|
fetchMock.mock(`end:/teams/123/accesses/789/`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
ok: true,
|
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();
|
expect(onClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to update the role', async () => {
|
it('fails to update the role', async () => {
|
||||||
fetchMock.patchOnce(`/api/teams/123/accesses/789/`, {
|
fetchMock.patchOnce(`end:/teams/123/accesses/789/`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
body: {
|
body: {
|
||||||
detail: 'The server is totally broken',
|
detail: 'The server is totally broken',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe('PanelTeams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders with no team to display', async () => {
|
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,
|
count: 0,
|
||||||
results: [],
|
results: [],
|
||||||
});
|
});
|
||||||
@@ -40,7 +40,7 @@ describe('PanelTeams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders an empty team', async () => {
|
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,
|
count: 1,
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
@@ -61,7 +61,7 @@ describe('PanelTeams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders a team with only 1 member', async () => {
|
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,
|
count: 1,
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@ describe('PanelTeams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders a non-empty team', async () => {
|
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,
|
count: 1,
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
@@ -115,7 +115,7 @@ describe('PanelTeams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders the error', async () => {
|
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,
|
status: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ describe('PanelTeams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders with team panel open', async () => {
|
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,
|
count: 1,
|
||||||
results: [],
|
results: [],
|
||||||
});
|
});
|
||||||
@@ -146,7 +146,7 @@ describe('PanelTeams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes and opens the team panel', async () => {
|
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,
|
count: 1,
|
||||||
results: [],
|
results: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ backend:
|
|||||||
frontend:
|
frontend:
|
||||||
envVars:
|
envVars:
|
||||||
PORT: 8080
|
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
|
replicas: 1
|
||||||
command:
|
command:
|
||||||
|
|||||||
Reference in New Issue
Block a user