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.
- package/cli/compiler/client/index.ts +2 -1
- package/cli/compiler/common/babel/plugins/services.ts +13 -6
- package/cli/compiler/common/index.ts +6 -6
- package/cli/compiler/index.ts +35 -3
- package/client/app/index.ts +2 -0
- package/client/components/Dialog/Manager.tsx +3 -49
- package/client/components/index.ts +1 -2
- package/client/services/router/index.tsx +6 -16
- package/common/errors/index.tsx +12 -31
- package/doc/TODO.md +1 -1
- package/package.json +1 -1
- package/server/services/auth/index.ts +62 -27
- package/server/services/auth/router/request.ts +17 -6
- package/server/services/router/http/index.ts +1 -1
- package/server/services/router/response/index.ts +1 -1
- package/server/services/schema/request.ts +28 -10
- package/client/components/Button.tsx +0 -298
- package/client/components/Dialog/card.tsx +0 -208
|
@@ -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 ? '
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
package/cli/compiler/index.ts
CHANGED
|
@@ -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?:
|
|
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
|
|
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
|
+
}
|
package/client/app/index.ts
CHANGED
|
@@ -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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 |
|
|
59
|
+
export type Router = ClientRouter | TAnyRouter;
|
|
60
60
|
|
|
61
|
-
export type Request = ClientRequest<ClientRouter> | ServerRequest<
|
|
61
|
+
export type Request = ClientRequest<ClientRouter> | ServerRequest<TAnyRouter>;
|
|
62
62
|
|
|
63
|
-
export type Response = ClientResponse<ClientRouter> | ServerResponse<
|
|
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.
|
|
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
|
}
|
package/common/errors/index.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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,
|
|
292
|
+
case 401: return new AuthRequired( message, details["feature"], details["action"], details );
|
|
312
293
|
|
|
313
|
-
case 402: return new UpgradeRequired( message, 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
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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,
|
|
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',
|
|
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,
|
|
50
|
-
|
|
51
|
-
public check(role:
|
|
52
|
-
|
|
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
|
+
}
|
|
@@ -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:
|
|
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
|
|
54
|
+
const validationSchema = isZodSchema(fields) ? fields : zod.object(fields);
|
|
44
55
|
|
|
45
|
-
const preprocessedSchema = preprocessSchema(
|
|
56
|
+
//const preprocessedSchema = preprocessSchema(validationSchema);
|
|
46
57
|
|
|
47
|
-
return
|
|
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
|
-
}
|