Skip to content

Commit e92dae8

Browse files
committed
Merge 4.1
2 parents c75329f + caff371 commit e92dae8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+2187
-921
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
VALIDATE_EDITORCONFIG: false
3131
VALIDATE_JSCPD: false
3232
VALIDATE_MARKDOWN_PRETTIER: false
33-
DEFAULT_BRANCH: "origin/4.0"
33+
DEFAULT_BRANCH: "origin/4.1"
3434
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3535

3636
- uses: actions/cache@v4

admin/advanced-customization.md

+462
Large diffs are not rendered by default.

admin/authentication-support.md

+235-35
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,55 @@
11
# Authentication Support
22

33
API Platform Admin delegates the authentication support to React Admin.
4-
Refer to [the chapter dedicated to authentication in the React Admin documentation](https://marmelab.com/react-admin/Authentication.html)
5-
for more information.
64

7-
In short, you have to tweak the data provider and the API documentation parser like this:
5+
Refer to the [Auth Provider Setup](https://marmelab.com/react-admin/Authentication.html) documentation for more information.
6+
7+
**Tip:** Once you have set up the authentication, you can also configure React Admin to perform client-side Authorization checks. Refer to the [Authorization](https://marmelab.com/react-admin/Permissions.html) documentation for more information.
8+
9+
## HydraAdmin
10+
11+
Enabling authentication support for [`<HydraAdmin>` component](./components.md#hydra) consists of a few parts, which need to be integrated together.
12+
13+
In the following steps, we will see how to:
14+
15+
- Make authenticated requests to the API (i.e. include the `Authorization` header)
16+
- Redirect users to the login page if they are not authenticated
17+
- Clear expired tokens when encountering unauthorized `401` response
18+
19+
### Make Authenticated Requests
20+
21+
First, we need to implement a `getHeaders` function, that will add the Bearer token from `localStorage` (if there is one) to the `Authorization` header.
822

923
```typescript
10-
// components/admin/Admin.tsx
24+
const getHeaders = () =>
25+
localStorage.getItem("token")
26+
? { Authorization: `Bearer ${localStorage.getItem("token")}` }
27+
: {};
28+
```
1129

12-
import Head from "next/head";
13-
import { useState } from "react";
14-
import { Navigate, Route } from "react-router-dom";
15-
import { CustomRoutes } from "react-admin";
30+
Then, extend the Hydra `fetch` function to use the `getHeaders` function to add the `Authorization` header to the requests.
31+
32+
```typescript
1633
import {
17-
fetchHydra as baseFetchHydra,
18-
HydraAdmin,
19-
hydraDataProvider as baseHydraDataProvider,
20-
useIntrospection,
34+
fetchHydra as baseFetchHydra,
2135
} from "@api-platform/admin";
22-
import { parseHydraDocumentation } from "@api-platform/api-doc-parser";
23-
import authProvider from "utils/authProvider";
24-
import { ENTRYPOINT } from "config/entrypoint";
2536

26-
const getHeaders = () => localStorage.getItem("token") ? {
27-
Authorization: `Bearer ${localStorage.getItem("token")}`,
28-
} : {};
2937
const fetchHydra = (url, options = {}) =>
3038
baseFetchHydra(url, {
3139
...options,
3240
headers: getHeaders,
3341
});
42+
43+
```
44+
45+
### Redirect To Login Page
46+
47+
Then, we'll create a `<RedirectToLogin>` component, that will redirect users to the `/login` route if no token is available in the `localStorage`, and call the dataProvider's `introspect` function otherwise.
48+
49+
```tsx
50+
import { Navigate } from "react-router-dom";
51+
import { useIntrospection } from "@api-platform/admin";
52+
3453
const RedirectToLogin = () => {
3554
const introspect = useIntrospection();
3655

@@ -40,36 +59,83 @@ const RedirectToLogin = () => {
4059
}
4160
return <Navigate to="/login" />;
4261
};
62+
```
63+
64+
### Clear Expired Tokens
65+
66+
Now, we will extend the `parseHydraDocumentaion` function (imported from the [@api-platform/api-doc-parser](https://github.com/api-platform/api-doc-parser) library).
67+
68+
We will customize it to clear expired tokens when encountering unauthorized `401` response.
69+
70+
```typescript
71+
import { parseHydraDocumentation } from "@api-platform/api-doc-parser";
72+
import { ENTRYPOINT } from "config/entrypoint";
73+
4374
const apiDocumentationParser = (setRedirectToLogin) => async () => {
4475
try {
4576
setRedirectToLogin(false);
46-
4777
return await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders });
4878
} catch (result) {
4979
const { api, response, status } = result;
5080
if (status !== 401 || !response) {
5181
throw result;
5282
}
5383

54-
// Prevent infinite loop if the token is expired
5584
localStorage.removeItem("token");
56-
5785
setRedirectToLogin(true);
5886

59-
return {
60-
api,
61-
response,
62-
status,
63-
};
87+
return { api, response, status };
6488
}
6589
};
66-
const dataProvider = (setRedirectToLogin) => baseHydraDataProvider({
67-
entrypoint: ENTRYPOINT,
68-
httpClient: fetchHydra,
69-
apiDocumentationParser: apiDocumentationParser(setRedirectToLogin),
70-
});
90+
```
91+
92+
### Extend The Data Provider
93+
94+
Now, we can initialize the Hydra data provider with the custom `fetchHydra` (with custom headers) and `apiDocumentationParser` functions created earlier.
95+
96+
```typescript
97+
import {
98+
hydraDataProvider as baseHydraDataProvider,
99+
} from "@api-platform/admin";
100+
import { ENTRYPOINT } from "config/entrypoint";
101+
102+
const dataProvider = (setRedirectToLogin) =>
103+
baseHydraDataProvider({
104+
entrypoint: ENTRYPOINT,
105+
httpClient: fetchHydra,
106+
apiDocumentationParser: apiDocumentationParser(setRedirectToLogin),
107+
});
108+
```
109+
110+
### Update The Admin Component
111+
112+
Lastly, we can stitch everything together in the `Admin` component.
113+
114+
```tsx
115+
// src/Admin.tsx
116+
117+
import Head from "next/head";
118+
import { useState } from "react";
119+
import { Navigate, Route } from "react-router-dom";
120+
import { CustomRoutes } from "react-admin";
121+
import {
122+
fetchHydra as baseFetchHydra,
123+
HydraAdmin,
124+
hydraDataProvider as baseHydraDataProvider,
125+
useIntrospection,
126+
} from "@api-platform/admin";
127+
import { parseHydraDocumentation } from "@api-platform/api-doc-parser";
128+
import authProvider from "utils/authProvider";
129+
import { ENTRYPOINT } from "config/entrypoint";
71130

72-
const Admin = () => {
131+
// Functions and components created in the previous steps:
132+
const getHeaders = () => {...};
133+
const fetchHydra = (url, options = {}) => {...};
134+
const RedirectToLogin = () => {...};
135+
const apiDocumentationParser = (setRedirectToLogin) => async () => {...};
136+
const dataProvider = (setRedirectToLogin) => {...};
137+
138+
export const Admin = () => {
73139
const [redirectToLogin, setRedirectToLogin] = useState(false);
74140

75141
return (
@@ -78,7 +144,11 @@ const Admin = () => {
78144
<title>API Platform Admin</title>
79145
</Head>
80146

81-
<HydraAdmin dataProvider={dataProvider(setRedirectToLogin)} authProvider={authProvider} entrypoint={window.origin}>
147+
<HydraAdmin
148+
dataProvider={dataProvider(setRedirectToLogin)}
149+
authProvider={authProvider}
150+
entrypoint={window.origin}
151+
>
82152
{redirectToLogin ? (
83153
<CustomRoutes>
84154
<Route path="/" element={<RedirectToLogin />} />
@@ -93,8 +163,138 @@ const Admin = () => {
93163
</HydraAdmin>
94164
</>
95165
);
166+
};
167+
```
168+
169+
### Example Implementation
170+
171+
For the implementation of the admin component, you can find a working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/4.0/pwa/components/admin/Admin.tsx).
172+
173+
## OpenApiAdmin
174+
175+
This section explains how to set up and customize the [`<OpenApiAdmin>` component](./components.md/#openapi) to enable authentication.
176+
177+
In the following steps, we will see how to:
178+
179+
- Make authenticated requests to the API (i.e. include the `Authorization` header)
180+
- Implement an authProvider to redirect users to the login page if they are not authenticated, and clear expired tokens when encountering unauthorized `401` response
181+
182+
### Making Authenticated Requests
183+
184+
First, we need to create a custom `httpClient` to add authentication tokens (via the the `Authorization` HTTP header) to requests.
185+
186+
We will then configure `openApiDataProvider` to use [`ra-data-simple-rest`](https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/README.md), a simple REST dataProvider for React Admin, and make it use the `httpClient` we created earlier.
187+
188+
```typescript
189+
// src/dataProvider.ts
190+
191+
const getAccessToken = () => localStorage.getItem("token");
192+
193+
const httpClient = async (url: string, options: fetchUtils.Options = {}) => {
194+
options.headers = new Headers({
195+
...options.headers,
196+
Accept: 'application/json',
197+
}) as Headers;
198+
199+
const token = getAccessToken();
200+
options.user = { token: `Bearer ${token}`, authenticated: !!token };
201+
202+
return await fetchUtils.fetchJson(url, options);
203+
};
204+
205+
const dataProvider = openApiDataProvider({
206+
dataProvider: simpleRestProvider(API_ENTRYPOINT_PATH, httpClient),
207+
entrypoint: API_ENTRYPOINT_PATH,
208+
docEntrypoint: API_DOCS_PATH,
209+
});
210+
```
211+
212+
**Note:** The `simpleRestProvider` provider expect the API to include a `Content-Range` header in the response. You can find more about the header syntax in the [Mozilla’s MDN documentation: Content-Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range).
213+
214+
**Note:** The `getAccessToken` function retrieves the JWT token stored in the browser's localStorage. Replace it with your own logic in case you don't store the token that way.
215+
216+
### Creating The AuthProvider
217+
218+
Now let's create and export an `authProvider` object that handles authentication and authorization logic.
219+
220+
```typescript
221+
// src/authProvider.ts
222+
223+
interface JwtPayload {
224+
sub: string;
225+
username: string;
96226
}
97-
export default Admin;
227+
228+
const getAccessToken = () => localStorage.getItem("token");
229+
230+
const authProvider = {
231+
login: async ({username, password}: { username: string; password: string }) => {
232+
const request = new Request(API_AUTH_PATH, {
233+
method: "POST",
234+
body: JSON.stringify({ email: username, password }),
235+
headers: new Headers({ "Content-Type": "application/json" }),
236+
});
237+
238+
const response = await fetch(request);
239+
240+
if (response.status < 200 || response.status >= 300) {
241+
throw new Error(response.statusText);
242+
}
243+
244+
const auth = await response.json();
245+
localStorage.setItem("token", auth.token);
246+
},
247+
logout: () => {
248+
localStorage.removeItem("token");
249+
return Promise.resolve();
250+
},
251+
checkAuth: () => getAccessToken() ? Promise.resolve() : Promise.reject(),
252+
checkError: (error: { status: number }) => {
253+
const status = error.status;
254+
if (status === 401 || status === 403) {
255+
localStorage.removeItem("token");
256+
return Promise.reject();
257+
}
258+
259+
return Promise.resolve();
260+
},
261+
getIdentity: () => {
262+
const token = getAccessToken();
263+
264+
if (!token) return Promise.reject();
265+
266+
const decoded = jwtDecode<JwtPayload>(token);
267+
268+
return Promise.resolve({
269+
id: decoded.sub,
270+
fullName: decoded.username,
271+
avatar: "",
272+
});
273+
},
274+
getPermissions: () => Promise.resolve(""),
275+
};
276+
277+
export default authProvider;
98278
```
99279

100-
For the implementation of the auth provider, you can find a working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/main/pwa/utils/authProvider.tsx).
280+
### Updating The Admin Component
281+
282+
Finally, we can update the `Admin` component to use the `authProvider` and `dataProvider` we created earlier.
283+
284+
```tsx
285+
// src/Admin.tsx
286+
287+
import { OpenApiAdmin } from '@api-platform/admin';
288+
import authProvider from "./authProvider";
289+
import dataProvider from "./dataProvider";
290+
import { API_DOCS_PATH, API_ENTRYPOINT_PATH } from "./config/api";
291+
292+
export default () => (
293+
<OpenApiAdmin
294+
entrypoint={API_ENTRYPOINT_PATH}
295+
docEntrypoint={API_DOCS_PATH}
296+
dataProvider={dataProvider}
297+
authProvider={authProvider}
298+
/>
299+
);
300+
```

0 commit comments

Comments
 (0)