@venizia/ardor-admin 0.4.8-5 → 0.4.8-7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,7 +63,10 @@ export interface IAuthRejection {
63
63
  }
64
64
  /** The auth provider of an ARDOR application: react-admin's methods, the user's roles and a token refresh. */
65
65
  export interface IAuthProvider extends IReactAdminAuthProvider {
66
- /** The roles of the signed-in user. */
66
+ /**
67
+ * The roles of the signed-in user, as the browser holds them: they only show or hide interface, and
68
+ * the backend must check every permission on every request.
69
+ */
67
70
  getRoles: (params?: unknown) => Promise<Set<string>>;
68
71
  /** `true` when the session was refreshed, `false` when there is no refresh to make. */
69
72
  refreshToken: () => Promise<boolean>;
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { LoggerFactory } from '@venizia/ignis-helpers/core';
4
+ import { QueryClient } from '@tanstack/react-query';
4
5
  import { CoreAdmin, CustomRoutes, Resource } from 'ra-core';
5
6
  import { Provider as ReduxProvider } from 'react-redux';
6
7
  import { Route } from 'react-router-dom';
7
8
  import { Features } from '../features/feature.js';
9
+ import { CoreBindings } from '@venizia/ardor-kernel';
8
10
  import { ApplicationContext } from '@venizia/ardor-react';
9
11
  import { ApplicationProviders } from './resolve.js';
10
12
  const Wrapper = ({ applicationName = 'ArdorApplication', container, reduxStore, suspense, logger, children, }) => {
@@ -70,7 +72,31 @@ export const ArdorApplication = (props) => {
70
72
  const providers = React.useMemo(() => {
71
73
  return ApplicationProviders.resolve({ container });
72
74
  }, [container]);
73
- const adminProps = { ...providers, ...reactAdminProps };
75
+ // The cache is owned here, so it can be emptied when the session ends by any route (a sign-out
76
+ // button empties it itself; a failed refresh, or code that calls `cleanUp`, does not).
77
+ const queryClient = React.useMemo(() => {
78
+ return reactAdminProps.queryClient ?? new QueryClient();
79
+ }, [reactAdminProps.queryClient]);
80
+ React.useEffect(() => {
81
+ const authService = container.get({
82
+ key: CoreBindings.DEFAULT_AUTH_SERVICE,
83
+ isOptional: true,
84
+ });
85
+ return authService?.subscribe({
86
+ listener: () => {
87
+ if (!authService.getAuth()?.value) {
88
+ // After the redirect to the sign-in page, as react-admin's own sign-out does.
89
+ setTimeout(() => {
90
+ // Not when the next user signed in meanwhile.
91
+ if (!authService.getAuth()?.value) {
92
+ queryClient.clear();
93
+ }
94
+ }, 0);
95
+ }
96
+ },
97
+ });
98
+ }, [container, queryClient]);
99
+ const adminProps = { ...providers, ...reactAdminProps, queryClient };
74
100
  const isMessagesReady = useFeatureMessagesReady({
75
101
  features,
76
102
  i18nProvider: providers.i18nProvider,
@@ -1,18 +1,28 @@
1
1
  import type { Container } from '@venizia/ignis-inversion';
2
2
  import type { I18nProvider } from 'ra-core';
3
3
  import type { IAuthProvider, IDataProvider } from '../common/index.js';
4
+ interface IResolvedProviders {
5
+ dataProvider: IDataProvider;
6
+ authProvider?: IAuthProvider;
7
+ i18nProvider: I18nProvider;
8
+ }
4
9
  /** Resolves the providers `ArdorApplication` hands to react-admin. */
5
10
  export declare class ApplicationProviders {
6
11
  /**
7
12
  * What `ArdorApplication` resolves from the container when it mounts: the one place, so a test of
8
- * the boot resolves exactly what the app does.
13
+ * the boot resolves exactly what the app does. With `isAuthRequired: true`, for an application that
14
+ * always binds a sign-in, `authProvider` is typed as present and an unbound one throws.
9
15
  */
10
16
  static resolve(opts: {
11
17
  container: Container;
12
- }): {
13
- dataProvider: IDataProvider;
14
- authProvider?: IAuthProvider;
15
- i18nProvider: I18nProvider;
18
+ isAuthRequired: true;
19
+ }): IResolvedProviders & {
20
+ authProvider: IAuthProvider;
16
21
  };
22
+ static resolve(opts: {
23
+ container: Container;
24
+ isAuthRequired?: boolean;
25
+ }): IResolvedProviders;
17
26
  }
27
+ export {};
18
28
  //# sourceMappingURL=resolve.d.ts.map
@@ -1,12 +1,8 @@
1
1
  import { CoreBindings } from '@venizia/ardor-kernel';
2
2
  /** Resolves the providers `ArdorApplication` hands to react-admin. */
3
3
  export class ApplicationProviders {
4
- /**
5
- * What `ArdorApplication` resolves from the container when it mounts: the one place, so a test of
6
- * the boot resolves exactly what the app does.
7
- */
8
4
  static resolve(opts) {
9
- const { container } = opts;
5
+ const { container, isAuthRequired } = opts;
10
6
  const dataProvider = container.get({
11
7
  key: CoreBindings.DEFAULT_REST_DATA_PROVIDER,
12
8
  });
@@ -14,7 +10,7 @@ export class ApplicationProviders {
14
10
  // without an auth provider - every page is public.
15
11
  const authProvider = container.get({
16
12
  key: CoreBindings.DEFAULT_AUTH_PROVIDER,
17
- isOptional: true,
13
+ isOptional: !isAuthRequired,
18
14
  });
19
15
  const i18nProvider = container.get({
20
16
  key: CoreBindings.DEFAULT_I18N_PROVIDER,
@@ -68,8 +68,8 @@ export declare class DefaultAuthProvider<TResource extends string = string> exte
68
68
  private hookQueue;
69
69
  /** Where `login` posts: `paths.signIn`, else the IGNIS auth controller's route. */
70
70
  protected get signInPath(): string;
71
- /** Where the admin goes once `login` resolves: `paths.afterLogin`, else `/`. */
72
- protected get afterLoginPath(): string;
71
+ /** Where the admin goes once `login` resolves: `paths.afterLogin`, else `/`; `false` stays on the page. */
72
+ protected get afterLoginPath(): string | false;
73
73
  /** The route of the login page, which `checkAuth` and a `401` redirect to. */
74
74
  protected get loginPagePath(): string;
75
75
  /** The route a `403` redirects to; the application mounts it. */
@@ -171,9 +171,16 @@ export declare class DefaultAuthProvider<TResource extends string = string> exte
171
171
  private clearSession;
172
172
  /** Announces the session in the store, when there is one. */
173
173
  private startStoredSession;
174
+ /**
175
+ * Runs when the session is gone, however it ended (sign-out, a `401`, a failed refresh, other
176
+ * code calling `cleanUp`): removes what the user stored under the store prefix of this
177
+ * application (`@app/store/`, or `@app/store/<id>/` with an application `id`) in `localStorage` and `sessionStorage`, so the next user does not see it.
178
+ * Override it to keep preferences, or put them under another prefix.
179
+ */
180
+ protected clearStoredData(): void;
174
181
  /**
175
182
  * Posts the sign-in body to `signInPath`, stores the session and resolves `{ ...response,
176
- * redirectTo }`, where `redirectTo` is `afterLoginPath`. Rejects when the request fails, when the
183
+ * redirectTo }`, where `redirectTo` is `afterLoginPath` (`false` when it is `false`). Rejects when the request fails, when the
177
184
  * response carries no token or user id, or when `beforeSaveSession` throws; no session is stored
178
185
  * then. Typed `Promise<unknown>` so that a subclass override of `login` keeps compiling.
179
186
  */
@@ -196,7 +203,10 @@ export declare class DefaultAuthProvider<TResource extends string = string> exte
196
203
  getIdentity(_params?: unknown): Promise<any>;
197
204
  /** Resolves with nothing; the roles come from `getRoles`. */
198
205
  getPermissions(_params?: unknown): Promise<void>;
199
- /** Resolves the roles of the stored session. */
206
+ /**
207
+ * Resolves the roles of the stored session. They are read from browser storage, which the user can
208
+ * edit: use them to show or hide interface only, and let the backend check every permission.
209
+ */
200
210
  getRoles(_params?: unknown): Promise<Set<string>>;
201
211
  /**
202
212
  * Refreshes the session through the network service's `refresh`, the one place a refresh is made:
@@ -13,7 +13,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
13
13
  var DefaultAuthProvider_1;
14
14
  import { getError, inject } from '@venizia/ignis-inversion';
15
15
  import { removeDoubleSlashes } from 'ra-core';
16
- import { BaseProvider, CoreBindings, RequestMethods, } from '@venizia/ardor-kernel';
16
+ import { BaseProvider, BrowserStorage, CoreBindings, LocalStorageKeys, RequestMethods, StorageAreas, } from '@venizia/ardor-kernel';
17
17
  /** Why `onSessionCleared` was called. */
18
18
  export class SessionClearReasons {
19
19
  /** `logout` ran. */
@@ -84,7 +84,7 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
84
84
  get signInPath() {
85
85
  return this.authProviderOptions.paths?.signIn ?? '/auth/sign-in';
86
86
  }
87
- /** Where the admin goes once `login` resolves: `paths.afterLogin`, else `/`. */
87
+ /** Where the admin goes once `login` resolves: `paths.afterLogin`, else `/`; `false` stays on the page. */
88
88
  get afterLoginPath() {
89
89
  return this.authProviderOptions.paths?.afterLogin ?? '/';
90
90
  }
@@ -293,9 +293,17 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
293
293
  }
294
294
  /** Announces or ends the session after the service saved or cleared it. */
295
295
  syncSession() {
296
- return this.authService.getAuth()?.value
297
- ? this.startStoredSession()
298
- : this.endSession({ reason: this.clearing ?? SessionClearReasons.EXTERNAL });
296
+ if (this.authService.getAuth()?.value) {
297
+ return this.startStoredSession();
298
+ }
299
+ try {
300
+ this.clearStoredData();
301
+ }
302
+ catch (error) {
303
+ // An override that throws must not stop the session from ending.
304
+ this.logger.error('[clearStoredData] failed, the session ends anyway | error: %o', error);
305
+ }
306
+ return this.endSession({ reason: this.clearing ?? SessionClearReasons.EXTERNAL });
299
307
  }
300
308
  async clearSession(opts) {
301
309
  this.clearing = opts.reason;
@@ -315,10 +323,21 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
315
323
  }
316
324
  return this.startSession({ token, user: this.authService.getUser() });
317
325
  }
326
+ /**
327
+ * Runs when the session is gone, however it ended (sign-out, a `401`, a failed refresh, other
328
+ * code calling `cleanUp`): removes what the user stored under the store prefix of this
329
+ * application (`@app/store/`, or `@app/store/<id>/` with an application `id`) in `localStorage` and `sessionStorage`, so the next user does not see it.
330
+ * Override it to keep preferences, or put them under another prefix.
331
+ */
332
+ clearStoredData() {
333
+ const prefix = this.authService.keys.storePrefix;
334
+ BrowserStorage.removeWhere({ storage: StorageAreas.LOCAL, prefix });
335
+ BrowserStorage.removeWhere({ storage: StorageAreas.SESSION, prefix });
336
+ }
318
337
  // --- react-admin's auth provider -------------------------------------------------------------
319
338
  /**
320
339
  * Posts the sign-in body to `signInPath`, stores the session and resolves `{ ...response,
321
- * redirectTo }`, where `redirectTo` is `afterLoginPath`. Rejects when the request fails, when the
340
+ * redirectTo }`, where `redirectTo` is `afterLoginPath` (`false` when it is `false`). Rejects when the request fails, when the
322
341
  * response carries no token or user id, or when `beforeSaveSession` throws; no session is stored
323
342
  * then. Typed `Promise<unknown>` so that a subclass override of `login` keeps compiling.
324
343
  */
@@ -337,9 +356,10 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
337
356
  await this.startSession(stored?.value === session.token.value
338
357
  ? { token: stored, user: this.authService.getUser() }
339
358
  : { token: session.token, user: { userId: session.userId, username } });
359
+ const afterLogin = this.afterLoginPath;
340
360
  return {
341
361
  ...response,
342
- redirectTo: removeDoubleSlashes(`/${this.afterLoginPath}`),
362
+ redirectTo: afterLogin === false ? false : removeDoubleSlashes(`/${afterLogin}`),
343
363
  };
344
364
  }
345
365
  /** Clears the session and resolves. */
@@ -410,7 +430,10 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
410
430
  getPermissions(_params) {
411
431
  return Promise.resolve();
412
432
  }
413
- /** Resolves the roles of the stored session. */
433
+ /**
434
+ * Resolves the roles of the stored session. They are read from browser storage, which the user can
435
+ * edit: use them to show or hide interface only, and let the backend check every permission.
436
+ */
414
437
  getRoles(_params) {
415
438
  return Promise.resolve(this.authService.getRoles());
416
439
  }
@@ -440,6 +463,18 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
440
463
  return isRefreshed;
441
464
  }
442
465
  value(container) {
466
+ // A subclass of DefaultAuthService with its own constructor must forward the application info to
467
+ // super(info): without it the service keeps the unscoped session, silently shared with every other
468
+ // application of the origin. Refuse that here, where the application info is at hand.
469
+ const id = container.get({
470
+ key: CoreBindings.APPLICATION_INFO,
471
+ isOptional: true,
472
+ })?.id;
473
+ if (id !== undefined && this.authService.keys.token !== LocalStorageKeys.scoped({ id }).token) {
474
+ throw getError({
475
+ message: `[DefaultAuthProvider] The auth service does not use the application id '${id}': a subclass of DefaultAuthService with its own constructor passes the application info to super(info)`,
476
+ });
477
+ }
443
478
  // A subclass with its own constructor rarely forwards the optional 4th argument to `super`.
444
479
  const permissionProvider = this.permissionProvider ??
445
480
  container.get({
@@ -1,6 +1,6 @@
1
1
  import { type Container } from '@venizia/ignis-inversion';
2
2
  import type { CreateParams, CreateResult, DeleteManyParams, DeleteManyResult, DeleteParams, DeleteResult, GetListParams, GetListResult, GetManyParams, GetManyReferenceParams, GetManyReferenceResult, GetManyResult, GetOneParams, GetOneResult, Identifier, QueryFunctionContext, RaRecord, UpdateManyParams, UpdateManyResult, UpdateParams, UpdateResult } from 'ra-core';
3
- import { BaseProvider, DefaultNetworkRequestService, type IApplicationInfo, type INetworkResponse, type ICustomParams, type IRestDataProviderOptions, type ISendParams, type TRequestType } from '@venizia/ardor-kernel';
3
+ import { BaseProvider, type DefaultAuthService, DefaultNetworkRequestService, type IApplicationInfo, type INetworkResponse, type ICustomParams, type IRestDataProviderOptions, type ISendParams, type TRequestType } from '@venizia/ardor-kernel';
4
4
  import type { TExtraRequest } from '@venizia/ignis-kernel/repository';
5
5
  import type { IDataProvider, IRestRequestOptions } from '../common/index.js';
6
6
  import { type IListFilter } from './list-filter.js';
@@ -35,7 +35,8 @@ export declare class DefaultRestDataProvider<TResource extends string = string>
35
35
  protected restDataProviderOptions: IRestDataProviderOptions;
36
36
  protected applicationInfo: IApplicationInfo;
37
37
  private cachedNetworkService?;
38
- private authService?;
38
+ /** The auth service of the container, set when `value` builds the provider; pass it to the network service you return from `createNetworkService`. */
39
+ protected authService?: DefaultAuthService;
39
40
  constructor(restDataProviderOptions: IRestDataProviderOptions, applicationInfo: IApplicationInfo);
40
41
  /**
41
42
  * Builds the network service this provider sends through. Override it to return a subclass of
@@ -14,6 +14,25 @@ var DefaultRestDataProvider_1;
14
14
  import { getError, inject } from '@venizia/ignis-inversion';
15
15
  import { BaseProvider, CoreBindings, DefaultNetworkRequestService, ValueUtility, NetworkHeaders, RequestCountData, RequestMethods, RequestTypes, } from '@venizia/ardor-kernel';
16
16
  import { ListFilterBuilder } from './list-filter.js';
17
+ /**
18
+ * The path of one record: the id is a single path segment, whatever characters it holds. An empty
19
+ * id, `.` and `..` (dot segments even when written `%2E`) and a lone surrogate (which cannot be
20
+ * encoded) are refused, without naming the id.
21
+ */
22
+ const recordPaths = (opts) => {
23
+ let segment = '';
24
+ try {
25
+ segment = encodeURIComponent(`${opts.id}`);
26
+ // eslint-disable-next-line venizia/no-silent-catch -- a lone surrogate cannot be encoded: the empty segment below refuses it
27
+ }
28
+ catch {
29
+ segment = '';
30
+ }
31
+ if (segment === '' || segment === '.' || segment === '..') {
32
+ throw getError({ message: '[recordPaths] The record id is not a valid path segment' });
33
+ }
34
+ return [opts.resource, segment];
35
+ };
17
36
  /**
18
37
  * The react-admin data provider over an IGNIS REST API. Each method takes one `opts` object
19
38
  * (`{ resource, params }`) and resolves the result react-admin expects; it rejects with the error
@@ -167,7 +186,7 @@ let DefaultRestDataProvider = DefaultRestDataProvider_1 = class DefaultRestDataP
167
186
  resource,
168
187
  type: RequestTypes.GET_ONE,
169
188
  method: RequestMethods.GET,
170
- paths: [resource, `${params.id}`],
189
+ paths: recordPaths({ resource, id: params.id }),
171
190
  query: { ...queryKey, filter },
172
191
  });
173
192
  }
@@ -217,7 +236,7 @@ let DefaultRestDataProvider = DefaultRestDataProvider_1 = class DefaultRestDataP
217
236
  resource,
218
237
  type: RequestTypes.UPDATE,
219
238
  method: RequestMethods.PATCH,
220
- paths: [resource, `${params.id}`],
239
+ paths: recordPaths({ resource, id: params.id }),
221
240
  body: params.data,
222
241
  });
223
242
  }
@@ -265,7 +284,7 @@ let DefaultRestDataProvider = DefaultRestDataProvider_1 = class DefaultRestDataP
265
284
  resource,
266
285
  type: RequestTypes.DELETE,
267
286
  method: RequestMethods.DELETE,
268
- paths: [resource, `${params.id}`],
287
+ paths: recordPaths({ resource, id: params.id }),
269
288
  });
270
289
  }
271
290
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venizia/ardor-admin",
3
- "version": "0.4.8-5",
3
+ "version": "0.4.8-7",
4
4
  "description": "ARDOR react-admin adapter - the REST data provider, auth provider and i18n provider wired through IGNIS inversion, the ArdorApplication root component, the translate and notify hooks, and the English and Vietnamese message bundles.",
5
5
  "keywords": [
6
6
  "admin",
@@ -61,8 +61,8 @@
61
61
  "prepublishOnly": "make rebuild"
62
62
  },
63
63
  "dependencies": {
64
- "@venizia/ardor-kernel": "0.4.8-5",
65
- "@venizia/ardor-react": "0.4.8-5"
64
+ "@venizia/ardor-kernel": "0.4.8-7",
65
+ "@venizia/ardor-react": "0.4.8-7"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@happy-dom/global-registrator": "^21.0.4",
@@ -72,11 +72,11 @@
72
72
  "@testing-library/react": "^16.3.3",
73
73
  "@types/bun": "^1.4.3",
74
74
  "@types/react": "^19",
75
- "@venizia/dev-configs": "^0.4.8-2",
76
- "@venizia/ignis-filter": "^0.4.7-2",
77
- "@venizia/ignis-helpers": "^0.4.8-4",
78
- "@venizia/ignis-inversion": "^0.4.7-2",
79
- "@venizia/ignis-kernel": "^0.4.8-4",
75
+ "@venizia/dev-configs": "^0.4.8-3",
76
+ "@venizia/ignis-filter": "^0.4.7-4",
77
+ "@venizia/ignis-helpers": "^0.4.8-6",
78
+ "@venizia/ignis-inversion": "^0.4.7-3",
79
+ "@venizia/ignis-kernel": "^0.4.8-6",
80
80
  "eslint": "^10.13.0",
81
81
  "oxlint": "1.89.0",
82
82
  "oxlint-tsgolint": "7.0.2004",
@@ -94,10 +94,10 @@
94
94
  "peerDependencies": {
95
95
  "@reduxjs/toolkit": ">=2.13.0",
96
96
  "@tanstack/react-query": ">=5.104.5",
97
- "@venizia/ignis-filter": "^0.4.7-2",
98
- "@venizia/ignis-helpers": "^0.4.8-4",
99
- "@venizia/ignis-inversion": "^0.4.7-2",
100
- "@venizia/ignis-kernel": "^0.4.8-4",
97
+ "@venizia/ignis-filter": "^0.4.7-4",
98
+ "@venizia/ignis-helpers": "^0.4.8-6",
99
+ "@venizia/ignis-inversion": "^0.4.7-3",
100
+ "@venizia/ignis-kernel": "^0.4.8-6",
101
101
  "ra-core": ">=5.15.5",
102
102
  "ra-i18n-polyglot": ">=5.15.5",
103
103
  "react": ">=19.3.1",
@@ -124,7 +124,7 @@
124
124
  "react-redux",
125
125
  "react-router-dom"
126
126
  ],
127
- "limit": "10850 B"
127
+ "limit": "11200 B"
128
128
  }
129
129
  ],
130
130
  "engines": {