Skip to content

Commit 72f0db4

Browse files
authored
docs(admin): openapi custom data provider example (#2116)
1 parent 6d52907 commit 72f0db4

File tree

1 file changed

+207
-34
lines changed

1 file changed

+207
-34
lines changed

admin/authentication-support.md

Lines changed: 207 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,38 @@ API Platform Admin delegates the authentication support to React Admin.
44
Refer to [the chapter dedicated to authentication in the React Admin documentation](https://marmelab.com/react-admin/Authentication.html)
55
for more information.
66

7-
In short, you have to tweak the data provider and the API documentation parser like this:
7+
## HydraAdmin
8+
9+
The authentication layer for [HydraAdmin component](https://api-platform.com/docs/admin/components/#hydra)
10+
consists of a few parts, which need to be integrated together.
11+
12+
### Authentication
13+
14+
Add the Bearer token from `localStorage` to request headers.
815

916
```typescript
10-
// components/admin/Admin.tsx
17+
const getHeaders = () =>
18+
localStorage.getItem("token")
19+
? { Authorization: `Bearer ${localStorage.getItem("token")}` }
20+
: {};
21+
```
1122

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";
16-
import {
17-
fetchHydra as baseFetchHydra,
18-
HydraAdmin,
19-
hydraDataProvider as baseHydraDataProvider,
20-
useIntrospection,
21-
} 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";
23+
Extend the Hydra fetch function with custom headers for authentication.
2524

26-
const getHeaders = () => localStorage.getItem("token") ? {
27-
Authorization: `Bearer ${localStorage.getItem("token")}`,
28-
} : {};
25+
```typescript
2926
const fetchHydra = (url, options = {}) =>
3027
baseFetchHydra(url, {
3128
...options,
3229
headers: getHeaders,
3330
});
31+
32+
```
33+
34+
### Login Redirection
35+
36+
Redirect users to a `/login` path, if no token is available in the `localStorage`.
37+
38+
```typescript
3439
const RedirectToLogin = () => {
3540
const introspect = useIntrospection();
3641

@@ -40,34 +45,73 @@ const RedirectToLogin = () => {
4045
}
4146
return <Navigate to="/login" />;
4247
};
48+
```
49+
50+
### API Documentation Parsing
51+
52+
Extend the `parseHydraDocumentaion` function from the [API Doc Parser library](https://github.com/api-platform/api-doc-parser)
53+
to handle the documentation parsing. Customize it to clear
54+
expired tokens when encountering unauthorized `401` response.
55+
56+
```typescript
4357
const apiDocumentationParser = (setRedirectToLogin) => async () => {
4458
try {
4559
setRedirectToLogin(false);
46-
4760
return await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders });
4861
} catch (result) {
4962
const { api, response, status } = result;
5063
if (status !== 401 || !response) {
5164
throw result;
5265
}
5366

54-
// Prevent infinite loop if the token is expired
5567
localStorage.removeItem("token");
56-
5768
setRedirectToLogin(true);
5869

59-
return {
60-
api,
61-
response,
62-
status,
63-
};
70+
return { api, response, status };
6471
}
6572
};
66-
const dataProvider = (setRedirectToLogin) => baseHydraDataProvider({
67-
entrypoint: ENTRYPOINT,
68-
httpClient: fetchHydra,
69-
apiDocumentationParser: apiDocumentationParser(setRedirectToLogin),
70-
});
73+
```
74+
75+
### Data Provider
76+
77+
Initialize the hydra data provider with custom headers and the documentation parser.
78+
79+
```typescript
80+
const dataProvider = (setRedirectToLogin) =>
81+
baseHydraDataProvider({
82+
entrypoint: ENTRYPOINT,
83+
httpClient: fetchHydra,
84+
apiDocumentationParser: apiDocumentationParser(setRedirectToLogin),
85+
});
86+
```
87+
88+
### Export Admin Component
89+
90+
Export the Hydra admin component, and track the users' authentication status.
91+
92+
```typescript
93+
// components/admin/Admin.tsx
94+
95+
import Head from "next/head";
96+
import { useState } from "react";
97+
import { Navigate, Route } from "react-router-dom";
98+
import { CustomRoutes } from "react-admin";
99+
import {
100+
fetchHydra as baseFetchHydra,
101+
HydraAdmin,
102+
hydraDataProvider as baseHydraDataProvider,
103+
useIntrospection,
104+
} from "@api-platform/admin";
105+
import { parseHydraDocumentation } from "@api-platform/api-doc-parser";
106+
import authProvider from "utils/authProvider";
107+
import { ENTRYPOINT } from "config/entrypoint";
108+
109+
// Auth, Parser, Provider calls
110+
const getHeaders = () => {...};
111+
const fetchHydra = (url, options = {}) => {...};
112+
const RedirectToLogin = () => {...};
113+
const apiDocumentationParser = (setRedirectToLogin) => async () => {...};
114+
const dataProvider = (setRedirectToLogin) => {...};
71115

72116
const Admin = () => {
73117
const [redirectToLogin, setRedirectToLogin] = useState(false);
@@ -78,7 +122,11 @@ const Admin = () => {
78122
<title>API Platform Admin</title>
79123
</Head>
80124

81-
<HydraAdmin dataProvider={dataProvider(setRedirectToLogin)} authProvider={authProvider} entrypoint={window.origin}>
125+
<HydraAdmin
126+
dataProvider={dataProvider(setRedirectToLogin)}
127+
authProvider={authProvider}
128+
entrypoint={window.origin}
129+
>
82130
{redirectToLogin ? (
83131
<CustomRoutes>
84132
<Route path="/" element={<RedirectToLogin />} />
@@ -93,8 +141,133 @@ const Admin = () => {
93141
</HydraAdmin>
94142
</>
95143
);
96-
}
144+
};
97145
export default Admin;
98146
```
99147

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).
148+
### Additional Notes
149+
150+
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).
151+
152+
## OpenApiAdmin
153+
154+
This section explains how to set up and customize the [OpenApiAdmin component](https://api-platform.com/docs/admin/components/#openapi) authentication layer.
155+
It covers:
156+
* Creating a custom HTTP Client
157+
* Data and rest data provider configuration
158+
* Implementation of an auth provider
159+
160+
### Data Provider & HTTP Client
161+
162+
Create a custom HTTP client to add authentication tokens to request headers.
163+
Configure the `openApiDataProvider`, and
164+
inject the custom HTTP client into the [Simple REST Data Provider for React-Admin](https://github.com/Serind/ra-data-simple-rest).
165+
166+
**File:** `src/components/jsonDataProvider.tsx`
167+
```typescript
168+
const httpClient = async (url: string, options: fetchUtils.Options = {}) => {
169+
options.headers = new Headers({
170+
...options.headers,
171+
Accept: 'application/json',
172+
}) as Headers;
173+
174+
const token = getAccessToken();
175+
options.user = { token: `Bearer ${token}`, authenticated: !!token };
176+
177+
return await fetchUtils.fetchJson(url, options);
178+
};
179+
180+
const jsonDataProvider = openApiDataProvider({
181+
dataProvider: simpleRestProvider(API_ENTRYPOINT_PATH, httpClient),
182+
entrypoint: API_ENTRYPOINT_PATH,
183+
docEntrypoint: API_DOCS_PATH,
184+
});
185+
```
186+
187+
> [!NOTE]
188+
> The `simpleRestProvider` provider expect the API to include a `Content-Range` header in the response.
189+
> 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).
190+
>
191+
> The `getAccessToken` function retrieves the JWT token stored in the browser.
192+
193+
### Authentication and Authorization
194+
195+
Create and export an `authProvider` object that handles authentication and authorization logic.
196+
197+
**File:** `src/components/authProvider.tsx`
198+
```typescript
199+
interface JwtPayload {
200+
exp?: number;
201+
iat?: number;
202+
roles: string[];
203+
username: string;
204+
}
205+
206+
const authProvider = {
207+
login: async ({username, password}: { username: string; password: string }) => {
208+
const request = new Request(API_AUTH_PATH, {
209+
method: "POST",
210+
body: JSON.stringify({ email: username, password }),
211+
headers: new Headers({ "Content-Type": "application/json" }),
212+
});
213+
214+
const response = await fetch(request);
215+
216+
if (response.status < 200 || response.status >= 300) {
217+
throw new Error(response.statusText);
218+
}
219+
220+
const auth = await response.json();
221+
localStorage.setItem("token", auth.token);
222+
},
223+
logout: () => {
224+
localStorage.removeItem("token");
225+
return Promise.resolve();
226+
},
227+
checkAuth: () => getAccessToken() ? Promise.resolve() : Promise.reject(),
228+
checkError: (error: { status: number }) => {
229+
const status = error.status;
230+
if (status === 401 || status === 403) {
231+
localStorage.removeItem("token");
232+
return Promise.reject();
233+
}
234+
235+
return Promise.resolve();
236+
},
237+
getIdentity: () => {
238+
const token = getAccessToken();
239+
240+
if (!token) return Promise.reject();
241+
242+
const decoded = jwtDecode<JwtPayload>(token);
243+
244+
return Promise.resolve({
245+
id: "",
246+
fullName: decoded.username,
247+
avatar: "",
248+
});
249+
},
250+
getPermissions: () => Promise.resolve(""),
251+
};
252+
253+
export default authProvider;
254+
```
255+
256+
### Export OpenApiAdmin Component
257+
258+
**File:** `src/App.tsx`
259+
```typescript
260+
import {OpenApiAdmin} from '@api-platform/admin';
261+
import authProvider from "./components/authProvider";
262+
import jsonDataProvider from "./components/jsonDataProvider";
263+
import {API_DOCS_PATH, API_ENTRYPOINT_PATH} from "./config/api";
264+
265+
export default () => (
266+
<OpenApiAdmin
267+
entrypoint={API_ENTRYPOINT_PATH}
268+
docEntrypoint={API_DOCS_PATH}
269+
dataProvider={jsonDataProvider}
270+
authProvider={authProvider}
271+
/>
272+
);
273+
```

0 commit comments

Comments
 (0)