proteum 1.0.0-5 → 1.0.2

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.
@@ -228,8 +228,9 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
228
228
  ]),
229
229
  ],
230
230
 
231
+ // CSP-safe in development: avoid any `eval`-based source map mode.
231
232
  // https://webpack.js.org/configuration/devtool/#devtool
232
- devtool: dev ? 'eval-cheap-module-source-map' : 'source-map',
233
+ devtool: dev ? 'cheap-module-source-map' : 'source-map',
233
234
  /*devServer: {
234
235
  hot: true,
235
236
  },*/
@@ -242,10 +242,10 @@ function Plugin(babel, { app, side, debug }: TOptions) {
242
242
 
243
243
  // Count how many total imports we transform
244
244
  importedCount: number,
245
- routeMethods: string[],
246
245
  routePaths: Set<string>,
247
246
  routesIndex: TRoutesIndex,
248
247
  contextGuardedClassMethods: WeakSet<types.ClassMethod>,
248
+ routeDecoratedClassMethods: WeakSet<types.ClassMethod>,
249
249
 
250
250
  // For every local identifier, store info about how it should be rewritten
251
251
  imported: {
@@ -262,11 +262,11 @@ function Plugin(babel, { app, side, debug }: TOptions) {
262
262
  this.importedCount = 0;
263
263
  this.debug = debug || false;
264
264
 
265
- this.routeMethods = [];
266
265
  this.routePaths = new Set();
267
266
 
268
267
  this.routesIndex = getRoutesIndex(app.paths.root);
269
268
  this.contextGuardedClassMethods = new WeakSet();
269
+ this.routeDecoratedClassMethods = new WeakSet();
270
270
  },
271
271
 
272
272
  visitor: {
@@ -288,8 +288,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
288
288
 
289
289
  if (!isRoute) continue;
290
290
 
291
- const methodName = node.key.name;
292
- this.routeMethods.push( methodName );
291
+ this.routeDecoratedClassMethods.add(node);
293
292
 
294
293
  const routePath = getRoutePathFromDecoratorExpression(decorator.expression);
295
294
  if (routePath)
@@ -462,8 +461,9 @@ function Plugin(babel, { app, side, debug }: TOptions) {
462
461
  // Must have a method name
463
462
  if (path.node.key.type !== 'Identifier') return;
464
463
 
465
- // Init context
464
+ // Track whether this exact method is decorated with @Route.
466
465
  const methodName = path.node.key.name;
466
+ const isRouteMethod = this.routeDecoratedClassMethods.has(path.node);
467
467
  let params = path.node.params;
468
468
 
469
469
  // Prefix references
@@ -531,6 +531,13 @@ function Plugin(babel, { app, side, debug }: TOptions) {
531
531
  );
532
532
  }
533
533
  else if (ref.source === 'request') {
534
+ // Only route handlers receive the `context` parameter.
535
+ if (!isRouteMethod) {
536
+ throw subPath.buildCodeFrameError(
537
+ `@request import "${ref.local}" can only be used inside @Route methods (found in "${methodName}").`
538
+ );
539
+ }
540
+
534
541
  // this.app.Models.client.[identifier]
535
542
  // e.g. this.app.Models.client.MyModel
536
543
  subPath.replaceWith(
@@ -544,7 +551,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
544
551
  } });
545
552
 
546
553
  if (
547
- this.routeMethods.includes(methodName)
554
+ isRouteMethod
548
555
  &&
549
556
  path.node.params.length < 2
550
557
  ) {
@@ -54,11 +54,11 @@ export default function createCommonConfig(
54
54
  mode: dev ? 'development' : 'production',
55
55
 
56
56
  resolveLoader: {
57
- // Recherche des loaders dans framework/node_modules (psinon, webpack cherche dans le projet)
57
+ // Support both install modes:
58
+ // - npm i: loaders are often hoisted in app/node_modules
59
+ // - npm link: loaders often live in framework/node_modules
58
60
  modules: [
59
- // The line below is not necessary
60
- // By default, webpack is supposed to search in the project directory at first
61
- //cli.paths.appRoot + '/node_modules',
61
+ app.paths.root + '/node_modules',
62
62
  cli.paths.core.root + '/node_modules',
63
63
  cli.paths.core.cli + '/node_modules',
64
64
  ],
@@ -125,14 +125,14 @@ export default function createCommonConfig(
125
125
  bail: !dev,
126
126
 
127
127
  // Persistent cache speeds up cold starts and incremental rebuilds.
128
- cache: (dev || cli.args.cache === true) ? {
128
+ cache: /*(dev || cli.args.cache === true) ? {
129
129
  type: 'filesystem',
130
130
  cacheDirectory: path.join(app.paths.cache, 'webpack', side),
131
131
  compression: false,
132
132
  buildDependencies: {
133
133
  config: [__filename],
134
134
  },
135
- } : false,
135
+ } : */false,
136
136
 
137
137
  // Increase compilation performance
138
138
  profile: false,
@@ -287,6 +287,18 @@ declare module '@models/types' {
287
287
  export * from '@/var/prisma/index';
288
288
  }
289
289
 
290
+ declare module '@common/errors' {
291
+
292
+ export * from '@common/errors/index';
293
+ export { default } from '@common/errors/index';
294
+
295
+ export const AuthRequired: typeof import('@common/errors/index').AuthRequired<FeatureKeys>;
296
+ export type AuthRequired = import('@common/errors/index').AuthRequired<FeatureKeys>;
297
+
298
+ export const UpgradeRequired: typeof import('@common/errors/index').UpgradeRequired<FeatureKeys>;
299
+ export type UpgradeRequired = import('@common/errors/index').UpgradeRequired<FeatureKeys>;
300
+ }
301
+
290
302
  declare module '@request' {
291
303
 
292
304
  }
@@ -377,6 +389,14 @@ declare type ${appClassIdentifier} = import("@/server/.generated/app").default;
377
389
 
378
390
  declare module '@cli/app' {
379
391
 
392
+ type TSetupConfig<TConfig> =
393
+ TConfig extends (...args: any[]) => any ? TConfig
394
+ : TConfig extends Array<infer TItem> ? Array<TSetupConfig<TItem>>
395
+ : TConfig extends object ? {
396
+ [K in keyof TConfig]: TSetupConfig<TConfig[K]> | TServiceSetup | TServiceRef
397
+ }
398
+ : TConfig;
399
+
380
400
  type App = {
381
401
 
382
402
  env: TEnvConfig;
@@ -391,7 +411,7 @@ declare module '@cli/app' {
391
411
  // app.setup('User', 'Core/User')
392
412
  serviceName: TServiceName,
393
413
  servicePath: string,
394
- serviceConfig?: ${appClassIdentifier}[TServiceName]["config"]
414
+ serviceConfig?: TSetupConfig<${appClassIdentifier}[TServiceName]["config"]>
395
415
  ]) => TServiceSetup;
396
416
  }
397
417
  const app: App;
@@ -436,7 +456,7 @@ declare module '@server/app' {
436
456
 
437
457
  declare module '@request' {
438
458
  import type { TRouterContext } from '@server/services/router/response';
439
- const routerContext: TRouterContext<CrossPath["Router"]>;
459
+ const routerContext: TRouterContext<${appClassIdentifier}["Router"]>;
440
460
  export = routerContext;
441
461
  }
442
462
 
@@ -453,6 +473,18 @@ declare module '@models' {
453
473
 
454
474
  export = models;
455
475
  }
476
+
477
+ declare module '@common/errors' {
478
+
479
+ export * from '@common/errors/index';
480
+ export { default } from '@common/errors/index';
481
+
482
+ export const AuthRequired: typeof import('@common/errors/index').AuthRequired<FeatureKeys>;
483
+ export type AuthRequired = import('@common/errors/index').AuthRequired<FeatureKeys>;
484
+
485
+ export const UpgradeRequired: typeof import('@common/errors/index').UpgradeRequired<FeatureKeys>;
486
+ export type UpgradeRequired = import('@common/errors/index').UpgradeRequired<FeatureKeys>;
487
+ }
456
488
 
457
489
  declare module '@models/types' {
458
490
  export * from '@/var/prisma/index';
@@ -529,4 +561,4 @@ declare module '@models/types' {
529
561
 
530
562
  }
531
563
 
532
- }
564
+ }
@@ -120,6 +120,8 @@ export default abstract class Application {
120
120
  }
121
121
 
122
122
  public abstract handleError( error: CoreError | Error );
123
+
124
+ public abstract handleUpdate(): void;
123
125
 
124
126
  // TODO: move on app side
125
127
  public reportBug = (infos: TBugReportInfos) => fetch('/feedback/bug/ui', {
@@ -12,8 +12,6 @@ import { blurable, deepContains, focusContent } from '@client/utils/dom';
12
12
 
13
13
  // Specific
14
14
  import type Application from '../../app';
15
- import Card, { Props as CardInfos } from './card';
16
- import Button from '../Button';
17
15
 
18
16
  /*----------------------------------
19
17
  - TYPES: IMPORTATIONS
@@ -67,8 +65,6 @@ type DialogActions = {
67
65
 
68
66
  show: (...args: TDialogShowArgs ) => TDialogControls,
69
67
 
70
- confirm: (title: string, content: string | ComponentChild, defaultBtn: 'Yes'|'No') => TDialogControls,
71
-
72
68
  loading: (title: string) => TDialogControls,
73
69
 
74
70
  info: (...[title, content, boutons, options]: TToastShortcutArgs) => TDialogControls,
@@ -137,34 +133,11 @@ export const createDialog = (app: Application, isToast: boolean): DialogActions
137
133
  content: Content
138
134
  }
139
135
  }
140
-
141
- // modal.show({ title: 'supprimer', content: <>...</> })
142
- if (Content.constructor === Object) {
143
-
144
- const { content: CardContent, data = {}, ...propsToast } = Content as TOptsToast;
145
-
146
- let cardContent: ComponentChild;
147
- if (typeof CardContent === 'function') {
148
- cardContent = <CardContent {...propsRendu} {...data} />
149
- propsToast.boutons = null; // Component content = advanced content = should include buttons
150
- } else {
151
- cardContent = CardContent;
152
- }
153
-
154
- render = (
155
- <Card {...propsRendu} {...propsToast} isToast={isToast}>
156
- {cardContent}
157
- </Card>
158
- )
159
-
160
136
  // modal.show( ToastSupprimer )
161
137
  // -> Content is a component rendering a Card
162
- } else {
163
-
164
- render = (
165
- <Content {...propsRendu} isToast={isToast} />
166
- )
167
- }
138
+ render = (
139
+ <Content {...propsRendu} isToast={isToast} />
140
+ )
168
141
 
169
142
  // Chargeur de données
170
143
  /*if (('data' in ComposantCharge) && typeof ComposantCharge.data === 'function') {
@@ -202,25 +175,6 @@ export const createDialog = (app: Application, isToast: boolean): DialogActions
202
175
  setToasts: undefined as unknown as DialogActions["setToasts"],
203
176
  setModals: undefined as unknown as DialogActions["setModals"],
204
177
 
205
- confirm: (title: string, content: string | ComponentChild, defaultBtn: 'Yes'|'No' = 'No') => show<boolean>(({ close }) => (
206
- <div class="card col">
207
- <header>
208
- <h2>{title}</h2>
209
- </header>
210
- {typeof content === 'string' ? <p>{content}</p> : content}
211
- <footer class="row fill">
212
- <Button type={defaultBtn === 'Yes' ? 'primary' : undefined}
213
- onClick={() => close(true)}>
214
- Yes
215
- </Button>
216
- <Button type={defaultBtn === 'No' ? 'primary' : undefined}
217
- onClick={() => close(false)}>
218
- No
219
- </Button>
220
- </footer>
221
- </div>
222
- )),
223
-
224
178
  loading: (title: string) => show({
225
179
  title: title,
226
180
  type: 'loading'
@@ -1,2 +1 @@
1
- export { Link } from '../services/router/components/Link';
2
- export { default as Button } from './Button';
1
+ export { Link } from '../services/router/components/Link';
@@ -10,7 +10,8 @@ import ReactDOM from 'react-dom';
10
10
  import type {
11
11
  default as ServerRouter,
12
12
  Request as ServerRequest,
13
- Response as ServerResponse
13
+ Response as ServerResponse,
14
+ TAnyRouter
14
15
  } from '@server/services/router';
15
16
  import type { TBasicSSrData } from '@server/services/router/response';
16
17
 
@@ -23,7 +24,6 @@ import { getLayout } from '@common/router/layouts';
23
24
  import { getRegisterPageArgs, buildRegex } from '@common/router/register';
24
25
  import { TFetcherList } from '@common/router/request/api';
25
26
  import type { TFrontRenderer } from '@common/router/response/page';
26
- import Button from '../../components/Button';
27
27
 
28
28
  import App from '@client/app/component';
29
29
  import type ClientApplication from '@client/app';
@@ -56,11 +56,11 @@ const LogPrefix = '[router]'
56
56
  // Client router can handle Client requests AND Server requests (for pages only)
57
57
  export type { default as ClientResponse, TRouterContext } from "./response";
58
58
 
59
- export type Router = ClientRouter | ServerRouter;
59
+ export type Router = ClientRouter | TAnyRouter;
60
60
 
61
- export type Request = ClientRequest<ClientRouter> | ServerRequest<ServerRouter>;
61
+ export type Request = ClientRequest<ClientRouter> | ServerRequest<TAnyRouter>;
62
62
 
63
- export type Response = ClientResponse<ClientRouter> | ServerResponse<ServerRouter>;
63
+ export type Response = ClientResponse<ClientRouter> | ServerResponse<TAnyRouter>;
64
64
 
65
65
  /*----------------------------------
66
66
  - TYPES: ROUTES LOADING
@@ -360,17 +360,7 @@ export default class ClientRouter<
360
360
  } catch (e) {
361
361
  console.error(`Failed to fetch the route ${route.chunk}`, e);
362
362
  try {
363
- this.context.modal.show(() => (
364
- <div class="card col bg white w-3">
365
- <h2>New Update Available!</h2>
366
- <p>
367
- A new version of the website is available. Please refresh the page to continue.
368
- </p>
369
- <Button type="primary" onClick={() => window.location.reload()}>
370
- Reload
371
- </Button>
372
- </div>
373
- ));
363
+ this.app.handleUpdate();
374
364
  } catch (error) {}
375
365
  throw new Error("A new version of the website is available. Please refresh the page.");
376
366
  }
@@ -35,27 +35,6 @@ type TErrorDetails = {
35
35
  origin?: string,
36
36
  }
37
37
 
38
- /*----------------------------------
39
- - TYPES: AUTH REQUIRED FEATURE
40
- ----------------------------------*/
41
-
42
- /**
43
- * Global, augmentable feature catalog used to constrain the `feature` argument
44
- * of `AuthRequired`.
45
- *
46
- * Default behavior (no augmentation): `feature` stays a free-form string.
47
- * App behavior (augmentation provided by the host app): `feature` becomes a
48
- * curated union of feature keys.
49
- */
50
- declare global {
51
- interface TAuthRequiredFeatureCatalog {}
52
- }
53
-
54
- type TAuthRequiredFeatureKey = Extract<keyof TAuthRequiredFeatureCatalog, string>;
55
- type TAuthRequiredFeature = [TAuthRequiredFeatureKey] extends [never]
56
- ? string
57
- : TAuthRequiredFeatureKey;
58
-
59
38
  /*----------------------------------
60
39
  - TYPES: BUG REPORT
61
40
  ----------------------------------*/
@@ -179,46 +158,48 @@ export class InputErrorSchema extends CoreError {
179
158
  }
180
159
  }
181
160
 
182
- export class AuthRequired extends CoreError {
161
+ export class AuthRequired<FeatureKeys extends string> extends CoreError {
183
162
  public http = 401;
184
163
  public title = "Authentication Required";
185
164
  public static msgDefaut = "Please Login to Continue.";
186
165
 
187
- public constructor(message: string, feature?: TAuthRequiredFeature);
188
- public constructor(message: string, motivation: string | undefined, details: TErrorDetails | undefined);
189
166
  public constructor(
190
167
  message: string,
191
- public feature?: string,
168
+ public feature: FeatureKeys,
169
+ public action: string,
192
170
  details?: TErrorDetails
193
171
  ) {
194
172
  super(message, details);
195
173
  }
196
174
 
197
- public json(): TJsonError & { feature?: string } {
175
+ public json(): TJsonError & { feature: string, action: string } {
198
176
  return {
199
177
  ...super.json(),
200
178
  feature: this.feature,
179
+ action: this.action,
201
180
  }
202
181
  }
203
182
  }
204
183
 
205
- export class UpgradeRequired extends CoreError {
184
+ export class UpgradeRequired<FeatureKeys extends string> extends CoreError {
206
185
  public http = 402;
207
186
  public title = "Upgrade Required";
208
187
  public static msgDefaut = "Please Upgrade to Continue.";
209
188
 
210
189
  public constructor(
211
190
  message: string,
212
- public feature: string,
191
+ public feature: FeatureKeys,
192
+ public action: string,
213
193
  details?: TErrorDetails
214
194
  ) {
215
195
  super(message, details);
216
196
  }
217
197
 
218
- public json(): TJsonError & { feature: string } {
198
+ public json(): TJsonError & { feature: string, action: string } {
219
199
  return {
220
200
  ...super.json(),
221
201
  feature: this.feature,
202
+ action: this.action,
222
203
  }
223
204
  }
224
205
  }
@@ -308,9 +289,9 @@ export const fromJson = ({ code, message, ...details }: TJsonError) => {
308
289
  else
309
290
  return new InputError( message, details );
310
291
 
311
- case 401: return new AuthRequired( message, (details as any).feature, details );
292
+ case 401: return new AuthRequired( message, details["feature"], details["action"], details );
312
293
 
313
- case 402: return new UpgradeRequired( message, details.feature, details );
294
+ case 402: return new UpgradeRequired( message, details["feature"], details["action"], details );
314
295
 
315
296
  case 403: return new Forbidden( message, details );
316
297
 
package/doc/TODO.md CHANGED
@@ -34,7 +34,7 @@ abstract class Controller<
34
34
 
35
35
  ```typescript
36
36
  //? /headhunter/missions/suggested'
37
- class Missions extends Controller<CrossPath["router"]> {
37
+ class Missions extends Controller<UniqueDomains["router"]> {
38
38
 
39
39
  auth = 'USER';
40
40
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
- "version": "1.0.0-5",
4
+ "version": "1.0.2",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -20,10 +20,47 @@ import { InputError, AuthRequired, Forbidden } from '@common/errors';
20
20
  - TYPES
21
21
  ----------------------------------*/
22
22
 
23
- export type TUserRole = typeof UserRoles[number]
23
+ declare global {
24
+ /**
25
+ * Optional app-level role registry.
26
+ *
27
+ * Apps can add their own roles (keys are role ids):
28
+ * `interface ProteumAuthRoleCatalog { GOD: true }`
29
+ */
30
+ interface ProteumAuthRoleCatalog {}
31
+
32
+ /**
33
+ * App-level feature catalog consumed by auth permission checks.
34
+ *
35
+ * Apps can augment this interface with their own feature map:
36
+ * `interface ProteumAuthFeatureCatalog extends MyFeatures {}`
37
+ */
38
+ interface ProteumAuthFeatureCatalog {}
39
+
40
+ /**
41
+ * Canonical feature keys union used across app + framework.
42
+ *
43
+ * Notes:
44
+ * - If the app does not define a feature catalog, this defaults to `string`.
45
+ * - Otherwise it becomes the string keys of `ProteumAuthFeatureCatalog`.
46
+ */
47
+ type FeatureKeys = (
48
+ keyof ProteumAuthFeatureCatalog extends never
49
+ ? string
50
+ : Extract<keyof ProteumAuthFeatureCatalog, string>
51
+ );
52
+ }
53
+
54
+ export type TUserRole = (
55
+ typeof UserRoles[number]
56
+ |
57
+ Extract<keyof ProteumAuthRoleCatalog, string>
58
+ );
24
59
 
25
60
  export type THttpRequest = express.Request | http.IncomingMessage;
26
61
 
62
+ export type TFeatureKey = FeatureKeys;
63
+
27
64
  /*----------------------------------
28
65
  - CONFIG
29
66
  ----------------------------------*/
@@ -167,7 +204,7 @@ export default abstract class AuthService<
167
204
  return session;
168
205
  }
169
206
 
170
- public createSession( session: TJwtSession, request: TRequest ): string {
207
+ public createSession( session: TJwtSession, request2: TRequest ): string {
171
208
 
172
209
  this.config.debug && console.info(LogPrefix, `Creating new session:`, session);
173
210
 
@@ -175,7 +212,7 @@ export default abstract class AuthService<
175
212
 
176
213
  this.config.debug && console.info(LogPrefix, `Generated JWT token for session:` + token);
177
214
 
178
- request.res.cookie('authorization', token, {
215
+ request2.res.cookie('authorization', token, {
179
216
  maxAge: this.config.jwt.expiration,
180
217
  });
181
218
 
@@ -191,30 +228,28 @@ export default abstract class AuthService<
191
228
  request.res.clearCookie('authorization');
192
229
  }
193
230
 
194
- public check(
195
- request: TRequest,
196
- role: TUserRole,
197
- motivation?: string,
198
- dataForDebug?: { [key: string]: any }
199
- ): TUser;
200
-
201
- public check(
202
- request: TRequest,
203
- role: false,
204
- motivation?: string,
205
- dataForDebug?: { [key: string]: any }
206
- ): null;
207
-
208
- public check(
209
- request: TRequest,
210
- role: TUserRole | false = 'USER',
211
- motivation?: string,
212
- dataForDebug?: { [key: string]: any }
231
+ public check(
232
+ request: TRequest,
233
+ role?: TUserRole | false,
234
+ ): TUser | null;
235
+
236
+ public check(
237
+ request: TRequest,
238
+ role: TUserRole | false,
239
+ feature: FeatureKeys,
240
+ action?: string,
241
+ ): TUser | null;
242
+
243
+ public check(
244
+ request: TRequest,
245
+ role: TUserRole | false = 'USER',
246
+ feature?: FeatureKeys,
247
+ action?: string,
213
248
  ): TUser | null {
214
249
 
215
250
  const user = request.user;
216
251
 
217
- this.config.debug && console.warn(LogPrefix, `Check auth, role = ${role}. Current user =`, user?.name, motivation);
252
+ this.config.debug && console.warn(LogPrefix, `Check auth, role = ${role}. Current user =`, user?.name, feature);
218
253
 
219
254
  if (user === undefined) {
220
255
 
@@ -223,13 +258,13 @@ export default abstract class AuthService<
223
258
  // Shoudln't be logged
224
259
  } else if (role === false) {
225
260
 
226
- return user;
261
+ return user as TUser;
227
262
 
228
263
  // Not connected
229
264
  } else if (user === null) {
230
265
 
231
266
  console.warn(LogPrefix, "Refusé pour anonyme (" + request.ip + ")");
232
- throw new AuthRequired('Please login to continue', motivation, dataForDebug);
267
+ throw new AuthRequired('Please login to continue', feature as any, action as any);
233
268
 
234
269
  // Insufficient permissions
235
270
  } else if (!user.roles.includes(role)) {
@@ -244,7 +279,7 @@ export default abstract class AuthService<
244
279
 
245
280
  }
246
281
 
247
- return user;
282
+ return user as TUser;
248
283
  }
249
284
 
250
- }
285
+ }
@@ -8,7 +8,6 @@ import jwt from 'jsonwebtoken';
8
8
  // Core
9
9
  import type { default as Router, Request as ServerRequest, TAnyRouter } from '@server/services/router';
10
10
  import RequestService from '@server/services/router/request/service';
11
- import { InputError, AuthRequired, Forbidden } from '@common/errors';
12
11
 
13
12
  // Specific
14
13
  import type AuthenticationRouterService from '.';
@@ -45,10 +44,22 @@ export default class UsersRequestService<
45
44
  return this.users.logout( this.request );
46
45
  }
47
46
 
47
+ public check(): TUser;
48
+
48
49
  // TODO: return user type according to entity
49
- public check(role: TUserRole, motivation?: string, dataForDebug?: {}): TUser;
50
- public check(role: false, motivation?: string, dataForDebug?: {}): null;
51
- public check(role: TUserRole | boolean = 'USER', motivation?: string, dataForDebug?: {}): TUser | null {
52
- return this.users.check( this.request, role, motivation, dataForDebug );
50
+ public check(role: TUserRole, feature: null): TUser;
51
+
52
+ public check(role: false): null;
53
+
54
+ public check(role: TUserRole, feature: FeatureKeys, action?: string): TUser;
55
+
56
+ public check(role: false, feature: FeatureKeys, action?: string): null;
57
+
58
+ public check(
59
+ role: TUserRole | false = 'USER',
60
+ feature?: FeatureKeys | null,
61
+ action?: string
62
+ ) {
63
+ return this.users.check( this.request, role, feature, action ) as any;
53
64
  }
54
- }
65
+ }
@@ -193,7 +193,7 @@ export default class HttpServer {
193
193
 
194
194
  routes.use( csp.expressCspHeader({
195
195
  directives: {
196
- 'script-src': [csp.INLINE, csp.SELF,
196
+ 'script-src': [csp.INLINE, csp.SELF, csp.UNSAFE_EVAL,
197
197
  ...this.config.csp.scripts
198
198
  ]
199
199
  }
@@ -52,7 +52,7 @@ export type TRouterContext<TRouter extends TServerRouter> = (
52
52
  Router: TRouter,
53
53
  }
54
54
  & TRouterContextServices<TRouter>
55
- //& TRouterRequestContext<TRouter>
55
+ & TRouterRequestContext<TRouter>
56
56
  )
57
57
 
58
58
  export type TRouterContextServices<
@@ -4,7 +4,6 @@
4
4
 
5
5
  // Npm
6
6
  import zod from 'zod';
7
- import { SomeType } from 'zod/v4/core';
8
7
 
9
8
  // Core
10
9
  import {
@@ -24,6 +23,18 @@ export type TConfig = {
24
23
  debug?: boolean
25
24
  }
26
25
 
26
+ type TValidationSchema = zod.ZodTypeAny;
27
+ type TValidationShape = zod.ZodRawShape;
28
+
29
+ const isZodSchema = (fields: unknown): fields is TValidationSchema => {
30
+ return (
31
+ typeof fields === 'object'
32
+ && fields !== null
33
+ && 'safeParse' in fields
34
+ && typeof (fields as TValidationSchema).safeParse === 'function'
35
+ );
36
+ }
37
+
27
38
  /*----------------------------------
28
39
  - SERVICE
29
40
  ----------------------------------*/
@@ -32,18 +43,25 @@ export default(
32
43
  config: TConfig,
33
44
  router = request.router,
34
45
  app = router.app
35
- ) => ({
36
-
37
- ...schema,
46
+ ) => {
38
47
 
39
- validate( fields: zod.ZodSchema | { [key: string]: zod.ZodSchema } ) {
48
+ function validate<TSchema extends TValidationSchema>( fields: TSchema ): zod.output<TSchema>;
49
+ function validate<TShape extends TValidationShape>( fields: TShape ): zod.output<zod.ZodObject<TShape>>;
50
+ function validate( fields: TValidationSchema | TValidationShape ) {
40
51
 
41
52
  config.debug && console.log(LogPrefix, "Validate request data:", request.data);
42
53
 
43
- const schema = typeof fields === 'object' ? zod.object(fields) : fields;
54
+ const validationSchema = isZodSchema(fields) ? fields : zod.object(fields);
44
55
 
45
- const preprocessedSchema = preprocessSchema(schema);
56
+ //const preprocessedSchema = preprocessSchema(validationSchema);
46
57
 
47
- return preprocessedSchema.parse(request.data);
48
- },
49
- })
58
+ return validationSchema.parse(request.data);
59
+ }
60
+
61
+ return {
62
+
63
+ ...schema,
64
+
65
+ validate,
66
+ }
67
+ }
@@ -1,298 +0,0 @@
1
- /*----------------------------------
2
- - DEPENDANCES
3
- ----------------------------------*/
4
-
5
- // Npm
6
- import React from 'react';
7
- import { VNode, RefObject,ComponentChild } from 'preact';
8
-
9
- // Core
10
- import { shouldOpenNewTab } from '@client/services/router/components/Link';
11
- import { history } from '@client/services/router/request/history';
12
- import useContext from '@/client/context';
13
-
14
- /*----------------------------------
15
- - TYPES
16
- ----------------------------------*/
17
-
18
- export type Props = {
19
-
20
- id?: string,
21
- refElem?: RefObject<HTMLElement>,
22
-
23
- icon?: ComponentChild,
24
- iconR?: ComponentChild,
25
-
26
- prefix?: ComponentChild,
27
- suffix?: ComponentChild,
28
-
29
- tag?: "a" | "button",
30
- type?: 'guide' | 'primary' | 'secondary' | 'link',
31
- shape?: 'default' | 'icon' | 'tile' | 'pill' | 'custom',
32
- size?: TComponentSize,
33
- class?: string,
34
-
35
- state?: [string, React.StateUpdater<string>],
36
- active?: boolean,
37
- selected?: boolean,
38
- disabled?: boolean,
39
- loading?: boolean,
40
- autoFocus?: boolean,
41
- onClick?: (e: MouseEvent) => any,
42
- async?: boolean,
43
-
44
- submenu?: ComponentChild,
45
- nav?: boolean | 'exact'
46
-
47
- // SEO: if icon only, should provinde a hint (aria-label)
48
- } & ({
49
- hint: string,
50
- children?: ComponentChild | ComponentChild[],
51
- } | {
52
- children: ComponentChild | ComponentChild[],
53
- hint?: string,
54
- }) & (TButtonProps | TLinkProps)
55
-
56
- export type TButtonProps = React.JSX.HTMLAttributes<HTMLButtonElement>
57
-
58
- export type TLinkProps = React.JSX.HTMLAttributes<HTMLAnchorElement>
59
-
60
- /*----------------------------------
61
- - HELPERS
62
- ----------------------------------*/
63
- const trimSlash = (str: string): string => {
64
- return str.endsWith('/') ? str.slice(0, -1) : str;
65
- }
66
-
67
- const isCurrentUrl = (currentUrl: string, url: string, exact?: boolean) => {
68
- return (
69
- (exact && (url === currentUrl || trimSlash(url) === currentUrl))
70
- ||
71
- (!exact && currentUrl.startsWith(url))
72
- )
73
- }
74
-
75
- /*----------------------------------
76
- - CONTROLEUR
77
- ----------------------------------*/
78
- export default ({
79
-
80
- id,
81
-
82
- // Content
83
- icon, prefix,
84
- children,
85
- iconR, suffix,
86
- submenu,
87
- nav,
88
- hint,
89
-
90
- // Style
91
- class: className,
92
- shape,
93
- size,
94
- type,
95
-
96
- // Interactions
97
- active,
98
- selected,
99
- state: stateUpdater,
100
- disabled,
101
- loading,
102
- //autoFocus,
103
- async,
104
-
105
- // HTML attributes
106
- tag: Tag,
107
- refElem,
108
- ...props
109
-
110
- }: Props) => {
111
-
112
- const ctx = useContext();
113
- let [isSelected, setIsSelected] = React.useState(false);
114
- let [isActive, setIsActive] = React.useState(false);
115
- const [isLoading, setLoading] = React.useState(false);
116
-
117
- if (isLoading || loading) {
118
- icon = <i src="spin" />
119
- iconR = undefined;
120
- disabled = true;
121
- }
122
-
123
- if (stateUpdater && id !== undefined) {
124
- const [active, setActive] = stateUpdater;
125
- if (id === active)
126
- isSelected = true;
127
- props.onClick = () => setActive(id);
128
- }
129
-
130
- // Hint
131
- if (hint !== undefined) {
132
- props['aria-label'] = hint;
133
- props.title = hint;
134
- }
135
-
136
- // Shape classes
137
- const classNames: string[] = ['btn'];
138
- if (className)
139
- classNames.push(className);
140
-
141
- if (shape !== undefined) {
142
- if (shape === 'tile')
143
- classNames.push('col');
144
- else
145
- classNames.push(shape);
146
- }
147
-
148
- if (size !== undefined)
149
- classNames.push(size);
150
-
151
- if (icon) {
152
- if (children === undefined)
153
- classNames.push('icon');
154
- }
155
-
156
- // state classes
157
- const [isMouseDown, setMouseDown] = React.useState(false);
158
- props.onMouseDown = () => setMouseDown(true);
159
- props.onMouseUp = () => setMouseDown(false);
160
- props.onMouseLeave = () => setMouseDown(false);
161
-
162
- // Theming & state
163
- if (isMouseDown)
164
- classNames.push('pressed');
165
- else if (selected || isSelected === true)
166
- classNames.push('bg accent');
167
- else if (type !== undefined)
168
- classNames.push(type === 'link' ? type : (' bg ' + type));
169
-
170
- if (active || isActive === true)
171
- classNames.push('active');
172
-
173
- // Icon
174
- if (prefix === undefined && icon !== undefined)
175
- prefix = typeof icon === "string" ? <i class={"svg-" + icon} /> : icon;
176
- if (suffix === undefined && iconR !== undefined)
177
- suffix = typeof iconR === "string" ? <i class={"svg-" + iconR} /> : iconR;
178
-
179
- // Render
180
- if ('link' in props || Tag === "a") {
181
-
182
- // Link (only if enabled)
183
- if (!disabled) {
184
-
185
- props.href = props.link;
186
-
187
- // External = open in new tab by default
188
- if (shouldOpenNewTab( props.href, props.target ))
189
- props.target = '_blank';
190
- }
191
-
192
- // Nav
193
- if (nav && props.target === undefined) {
194
-
195
- const checkIfCurrentUrl = (url: string) =>
196
- isCurrentUrl(url, props.link, nav === 'exact');
197
-
198
- React.useEffect(() => {
199
-
200
- // Init
201
- if (checkIfCurrentUrl(ctx.request.path))
202
- setIsActive(true);
203
-
204
- // On location change
205
- return history?.listen(({ location }) => {
206
-
207
- setIsActive( checkIfCurrentUrl(location.pathname) );
208
-
209
- })
210
-
211
- }, []);
212
- }
213
-
214
- Tag = 'a';
215
-
216
- } else {
217
- Tag = 'button';
218
-
219
- // Avoid to trigget onclick when presing enter
220
- if (type !== 'primary')
221
- props.type = 'button';
222
- else
223
- props.type = 'submit';
224
- }
225
-
226
- let render: VNode = (
227
- <Tag {...props} id={id} class={classNames.join(' ')} disabled={disabled} ref={refElem} onClick={(e: MouseEvent) => {
228
-
229
- // annulation si:
230
- // - Pas clic gauche
231
- // - Event annulé
232
- if (e.button !== 0)
233
- return;
234
-
235
- // Disabled
236
- if (disabled)
237
- return false;
238
-
239
- // Custom event
240
- if (props.onClick !== undefined) {
241
-
242
- const returned = props.onClick(e);
243
- if (async && returned?.then) {
244
- setLoading(true);
245
- returned.finally(() => setLoading(false));
246
- }
247
- }
248
-
249
- // Link
250
- let nativeEvent: boolean = false;
251
- if (('link' in props) && !e.defaultPrevented) {
252
-
253
- // Nouvelle fenetre = event par defaut
254
- if (props.target === '_blank') {
255
-
256
- nativeEvent = true;
257
-
258
- // Page change = loading indicator
259
- } else if (props.target === "_self") {
260
-
261
- setLoading(true);
262
- window.location.href = props.link;
263
-
264
- } else {
265
-
266
- history?.push(props.link);
267
- }
268
- }
269
-
270
- if (!nativeEvent) {
271
- e.preventDefault();
272
- return false;
273
- }
274
- }}>
275
- {prefix}
276
- {children === undefined
277
- ? null
278
- : shape === 'custom'
279
- ? children : (
280
- <span class={"label"}>
281
- {children}
282
- </span>
283
- )}
284
- {suffix}
285
- </Tag>
286
- )
287
-
288
- if (Tag === "li" || submenu) {
289
- render = (
290
- <li>
291
- {render}
292
- {submenu}
293
- </li>
294
- )
295
- }
296
-
297
- return render;
298
- }
@@ -1,208 +0,0 @@
1
- /*----------------------------------
2
- - DEPENDANCES
3
- ----------------------------------*/
4
-
5
- // Npm
6
- import React from 'react';
7
-
8
- // Composants globaux
9
- import Button from '@client/components/Button';
10
-
11
- /*----------------------------------
12
- - TYPES
13
- ----------------------------------*/
14
- import { ComponentChild } from 'preact';
15
-
16
- type funcBtns = () => void
17
-
18
- type Boutons = { [label: string]: funcBtns }
19
-
20
- /*----------------------------------
21
- - CONTENU
22
- ----------------------------------*/
23
- export type Props = {
24
-
25
- // Informations modale
26
- type?: 'primary' | 'success' | 'warning' | 'error' | 'loading' | 'info',
27
- cover?: string,
28
- icon?: ComponentChild,
29
- title?: string | ComponentChild,
30
- className?: string,
31
-
32
- children?: ComponentChild,
33
- isToast?: boolean,
34
- width?: number,
35
-
36
- footer?: ComponentChild,
37
- boutons?: Boutons | null, // nul = pas de footer
38
- defaultBtn?: string,
39
-
40
- prison?: boolean,
41
- /* Hide after x seconds */autohide?: number | false,
42
- close?: funcBtns,
43
- onClose?: () => Promise<any>,
44
- }
45
-
46
- export default ({
47
-
48
- // Content
49
- type,
50
- cover,
51
- icon,
52
- title,
53
- className = '',
54
-
55
- children,
56
- isToast,
57
- width,
58
-
59
- footer,
60
- boutons,
61
- defaultBtn,
62
-
63
- onClose,
64
- close,
65
- autohide,
66
- prison,
67
-
68
- }: Props) => {
69
-
70
- // Boutons
71
- if (footer === undefined && boutons !== null) {
72
-
73
- // Default buttons
74
- if (boutons === undefined || !Object.keys(boutons).length) {
75
-
76
- // Toast: by default, if no buttons, we autohide after 3 seconds
77
- if (autohide === undefined)
78
- autohide = 3;
79
-
80
- // If isToast, we show a default OK button
81
- if (close && !isToast)
82
- boutons = { 'Ok': () => close(true) };
83
- else
84
- boutons = null;
85
-
86
- }
87
-
88
- if (boutons !== null) {
89
-
90
- const nbBtns = Object.keys(boutons).length;
91
-
92
- footer = Object.entries(boutons).map(([texte, action]: [string, Function], index: number) => {
93
- const dernier = nbBtns > 1 && index === nbBtns - 1;
94
- return (
95
- <Button
96
- async
97
- onClick={() => action()}
98
- type={(defaultBtn === undefined ? dernier : (defaultBtn === texte)) ? 'primary' : undefined}
99
- >
100
- {texte}
101
- </Button>
102
- )
103
- });
104
- }
105
- }
106
-
107
- if (typeof icon === 'string')
108
- icon = <i class={"svg-" + icon} />
109
- else if (icon === undefined)
110
- switch (type) {
111
- case 'info':
112
- icon = <i src="info-circle" />
113
- break;
114
- case 'success':
115
- icon = <i src="check-circle" />
116
- break;
117
- case 'warning':
118
- icon = <i src="exclamation-circle" />
119
- break;
120
- case 'error':
121
- icon = <i src="times-circle" />
122
- break;
123
- case 'loading':
124
- icon = <i src="spin" />
125
- break;
126
- }
127
-
128
- // Autohide
129
- if (isToast)
130
- React.useEffect(() => {
131
- if (autohide) {
132
- const timeout = setTimeout(() => close(true), autohide * 1000);
133
- return () => clearTimeout(timeout);
134
- }
135
- }, []);
136
-
137
- let render = isToast ? (
138
- <div class="card row bg dark" onClick={() => isToast && !prison && close(true)}>
139
-
140
- {icon}
141
-
142
- <div>
143
-
144
- {typeof title === "string" ? (
145
- <strong>{title}</strong>
146
- ) : title}
147
-
148
- {children}
149
-
150
- </div>
151
-
152
- </div>
153
- ) : (
154
- <div class={"card pd-2 col al-top " + className} style={width === undefined
155
- ? {}
156
- : { minWidth: width + "px", maxWidth: width + "px" }
157
- }>
158
-
159
- {(title || icon) && (
160
- <header {...{
161
- class: ('col ' + type),
162
- style: cover ? {
163
- backgroundImage: 'url(' + cover + ')'
164
- } : undefined
165
- }}>
166
-
167
- {icon}
168
-
169
- {typeof title === "string" ? (
170
- <strong>{title}</strong>
171
- ) : title}
172
-
173
- {(!prison && close) && (
174
- <Button class="close" icon="times" size="s" shape="pill" onClick={async () => {
175
- if (typeof close === "function") {
176
-
177
- if (onClose !== undefined)
178
- onClose(false);
179
- else
180
- close(false);
181
- }
182
- }} />
183
- )}
184
-
185
- </header>
186
- )}
187
-
188
- {children && (
189
- <div class="col content">
190
-
191
- {children}
192
-
193
- </div>
194
- )}
195
-
196
- {footer && (
197
- <footer class="row fill actions">
198
-
199
- {footer}
200
-
201
- </footer>
202
- )}
203
-
204
- </div>
205
- )
206
-
207
- return render;
208
- }