@@ -4,33 +4,38 @@ API Platform Admin delegates the authentication support to React Admin.
4
4
Refer to [ the chapter dedicated to authentication in the React Admin documentation] ( https://marmelab.com/react-admin/Authentication.html )
5
5
for more information.
6
6
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.
8
15
9
16
``` typescript
10
- // components/admin/Admin.tsx
17
+ const getHeaders = () =>
18
+ localStorage .getItem (" token" )
19
+ ? { Authorization: ` Bearer ${localStorage .getItem (" token" )} ` }
20
+ : {};
21
+ ```
11
22
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.
25
24
26
- const getHeaders = () => localStorage .getItem (" token" ) ? {
27
- Authorization: ` Bearer ${localStorage .getItem (" token" )} ` ,
28
- } : {};
25
+ ``` typescript
29
26
const fetchHydra = (url , options = {}) =>
30
27
baseFetchHydra (url , {
31
28
... options ,
32
29
headers: getHeaders ,
33
30
});
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
34
39
const RedirectToLogin = () => {
35
40
const introspect = useIntrospection ();
36
41
@@ -40,34 +45,73 @@ const RedirectToLogin = () => {
40
45
}
41
46
return <Navigate to =" /login" />;
42
47
};
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
43
57
const apiDocumentationParser = (setRedirectToLogin ) => async () => {
44
58
try {
45
59
setRedirectToLogin (false );
46
-
47
60
return await parseHydraDocumentation (ENTRYPOINT , { headers: getHeaders });
48
61
} catch (result ) {
49
62
const { api, response, status } = result ;
50
63
if (status !== 401 || ! response ) {
51
64
throw result ;
52
65
}
53
66
54
- // Prevent infinite loop if the token is expired
55
67
localStorage .removeItem (" token" );
56
-
57
68
setRedirectToLogin (true );
58
69
59
- return {
60
- api ,
61
- response ,
62
- status ,
63
- };
70
+ return { api , response , status };
64
71
}
65
72
};
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 ) => {... };
71
115
72
116
const Admin = () => {
73
117
const [redirectToLogin, setRedirectToLogin] = useState (false );
@@ -78,7 +122,11 @@ const Admin = () => {
78
122
<title >API Platform Admin < / title >
79
123
< / Head >
80
124
81
- < HydraAdmin dataProvider = {dataProvider(setRedirectToLogin )} authProvider = {authProvider } entrypoint = {window.origin }>
125
+ < HydraAdmin
126
+ dataProvider = {dataProvider(setRedirectToLogin )}
127
+ authProvider = {authProvider }
128
+ entrypoint = {window.origin }
129
+ >
82
130
{redirectToLogin ? (
83
131
<CustomRoutes >
84
132
<Route path = " /" element = {<RedirectToLogin />} / >
@@ -93,8 +141,133 @@ const Admin = () => {
93
141
< / HydraAdmin >
94
142
< / >
95
143
);
96
- }
144
+ };
97
145
export default Admin ;
98
146
```
99
147
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