proteum 2.0.0 → 2.1.0

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.
Files changed (94) hide show
  1. package/AGENTS.md +13 -1
  2. package/README.md +375 -0
  3. package/agents/framework/AGENTS.md +917 -0
  4. package/agents/project/AGENTS.md +138 -0
  5. package/agents/{codex → project}/CODING_STYLE.md +3 -2
  6. package/agents/project/client/AGENTS.md +108 -0
  7. package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
  8. package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
  9. package/agents/project/server/services/AGENTS.md +170 -0
  10. package/agents/{codex → project}/tests/AGENTS.md +1 -0
  11. package/cli/app/config.ts +3 -2
  12. package/cli/app/index.ts +6 -66
  13. package/cli/bin.js +7 -2
  14. package/cli/commands/build.ts +94 -27
  15. package/cli/commands/check.ts +15 -1
  16. package/cli/commands/dev.ts +288 -132
  17. package/cli/commands/doctor.ts +108 -0
  18. package/cli/commands/explain.ts +226 -0
  19. package/cli/commands/init.ts +76 -70
  20. package/cli/commands/lint.ts +18 -1
  21. package/cli/commands/refresh.ts +16 -6
  22. package/cli/commands/typecheck.ts +14 -1
  23. package/cli/compiler/artifacts/controllers.ts +150 -0
  24. package/cli/compiler/artifacts/discovery.ts +132 -0
  25. package/cli/compiler/artifacts/manifest.ts +267 -0
  26. package/cli/compiler/artifacts/routing.ts +315 -0
  27. package/cli/compiler/artifacts/services.ts +480 -0
  28. package/cli/compiler/artifacts/shared.ts +12 -0
  29. package/cli/compiler/client/identite.ts +2 -1
  30. package/cli/compiler/client/index.ts +13 -3
  31. package/cli/compiler/common/controllers.ts +23 -28
  32. package/cli/compiler/common/files/style.ts +3 -4
  33. package/cli/compiler/common/generatedRouteModules.ts +333 -19
  34. package/cli/compiler/common/proteumManifest.ts +133 -0
  35. package/cli/compiler/index.ts +33 -896
  36. package/cli/compiler/server/index.ts +21 -4
  37. package/cli/context.ts +71 -0
  38. package/cli/index.ts +39 -181
  39. package/cli/presentation/commands.ts +208 -0
  40. package/cli/presentation/compileReporter.ts +65 -0
  41. package/cli/presentation/devSession.ts +70 -0
  42. package/cli/presentation/help.ts +193 -0
  43. package/cli/presentation/ink.ts +69 -0
  44. package/cli/presentation/layout.ts +83 -0
  45. package/cli/runtime/argv.ts +49 -0
  46. package/cli/runtime/command.ts +25 -0
  47. package/cli/runtime/commands.ts +221 -0
  48. package/cli/runtime/importEsm.ts +7 -0
  49. package/cli/runtime/verbose.ts +15 -0
  50. package/cli/utils/agents.ts +5 -4
  51. package/cli/utils/keyboard.ts +12 -6
  52. package/client/app/index.ts +0 -6
  53. package/client/services/router/index.tsx +1 -1
  54. package/client/services/router/response/index.tsx +2 -2
  55. package/common/dev/serverHotReload.ts +12 -0
  56. package/common/router/index.ts +3 -2
  57. package/common/router/layouts.ts +1 -1
  58. package/common/router/pageSetup.ts +1 -0
  59. package/package.json +10 -8
  60. package/prettier/router-registration-plugin.cjs +52 -0
  61. package/prettier.config.cjs +1 -0
  62. package/scripts/cleanup-generated-controllers.ts +2 -2
  63. package/scripts/fix-reference-app-typing.ts +2 -2
  64. package/scripts/format-router-registrations.ts +119 -0
  65. package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
  66. package/scripts/refactor-server-controllers.ts +19 -18
  67. package/scripts/refactor-server-runtime-aliases.ts +1 -1
  68. package/server/app/commands.ts +309 -25
  69. package/server/app/container/config.ts +1 -1
  70. package/server/app/container/index.ts +2 -2
  71. package/server/app/controller/index.ts +13 -4
  72. package/server/app/index.ts +53 -37
  73. package/server/app/service/container.ts +26 -28
  74. package/server/app/service/index.ts +10 -20
  75. package/server/app.tsconfig.json +9 -2
  76. package/server/index.ts +32 -1
  77. package/server/services/auth/index.ts +234 -15
  78. package/server/services/auth/router/index.ts +39 -7
  79. package/server/services/auth/router/request.ts +40 -8
  80. package/server/services/disks/index.ts +1 -1
  81. package/server/services/prisma/Facet.ts +2 -2
  82. package/server/services/prisma/index.ts +22 -5
  83. package/server/services/prisma/mariadb.ts +47 -0
  84. package/server/services/router/http/index.ts +9 -1
  85. package/server/services/router/index.ts +10 -4
  86. package/server/services/router/response/index.ts +26 -6
  87. package/types/auth-check-rules.test.ts +51 -0
  88. package/types/controller-request-context.test.ts +55 -0
  89. package/types/service-config.test.ts +39 -0
  90. package/agents/codex/AGENTS.md +0 -95
  91. package/agents/codex/client/AGENTS.md +0 -102
  92. package/agents/codex/server/services/AGENTS.md +0 -137
  93. package/server/services/models.7z +0 -0
  94. /package/agents/{codex → project}/agents.md.zip +0 -0
@@ -3,16 +3,10 @@
3
3
  ----------------------------------*/
4
4
 
5
5
  // Specific
6
- import type {
7
- AnyService,
8
- StartedServicesIndex,
9
- // Hooks
10
- THookCallback,
11
- THooksIndex,
12
- } from '.';
6
+ import type { AnyService, AnyServiceClass, StartedServicesIndex } from '.';
13
7
 
14
8
  /*----------------------------------
15
- - TYPES: REGISTRATION
9
+ - TYPES
16
10
  ----------------------------------*/
17
11
 
18
12
  // From service/service.json
@@ -24,34 +18,38 @@ export type TServiceMetas<TServiceClass extends AnyService = AnyService> = {
24
18
  class: () => { default: ClassType<TServiceClass> };
25
19
  };
26
20
 
27
- export type TRegisteredService<TServiceClass extends AnyService = AnyService> = {
28
- type: 'service'; // Used to recognize if an object is a registered service
29
- config?: {};
30
- metas: TServiceMetas<TServiceClass>;
31
- hooks: THooksIndex<{}>;
32
- on: (hookName: string, hookFunc: THookCallback<any>) => void;
33
- subServices: TRegisteredServicesIndex;
34
- };
35
-
36
- export type TRegisteredServicesIndex<TServiceClass extends AnyService = AnyService> = {
37
- [serviceId: string]: TRegisteredService<TServiceClass>;
38
- };
39
-
40
- /*----------------------------------
41
- - CONFIG
42
- ----------------------------------*/
43
-
44
- const LogPrefix = '[service]';
21
+ export type ServiceConfig<TServiceClass extends AnyServiceClass> = NonNullable<ConstructorParameters<TServiceClass>[1]>;
22
+
23
+ type ExactConfig<TValue, TShape> = TValue extends TShape
24
+ ? TShape extends (...args: never[]) => infer _TReturn
25
+ ? TValue
26
+ : TValue extends readonly (infer TValueItem)[]
27
+ ? TShape extends readonly (infer TShapeItem)[]
28
+ ? readonly ExactConfig<TValueItem, TShapeItem>[]
29
+ : never
30
+ : TValue extends object
31
+ ? TShape extends object
32
+ ? Exclude<keyof TValue, keyof TShape> extends never
33
+ ? { [K in keyof TValue]: K extends keyof TShape ? ExactConfig<TValue[K], TShape[K]> : never }
34
+ : never
35
+ : TValue
36
+ : TValue
37
+ : never;
45
38
 
46
39
  /*----------------------------------
47
40
  - CLASS
48
41
  ----------------------------------*/
49
42
  export class ServicesContainer<TServicesIndex extends StartedServicesIndex = StartedServicesIndex> {
50
- public registered: TRegisteredServicesIndex = {};
51
-
52
43
  // All service instances by service id
53
44
  public allServices: TServicesIndex = {} as TServicesIndex;
54
45
 
46
+ public config<TServiceClass extends AnyServiceClass, const TConfig extends ServiceConfig<TServiceClass>>(
47
+ _serviceClass: TServiceClass,
48
+ config: TConfig & ExactConfig<TConfig, ServiceConfig<TServiceClass>>,
49
+ ): TConfig {
50
+ return config;
51
+ }
52
+
55
53
  public callableInstance = <TInstance extends object, TCallableName extends keyof TInstance>(
56
54
  instance: TInstance,
57
55
  funcName: TCallableName,
@@ -6,7 +6,6 @@
6
6
  import type { Application } from '../index';
7
7
  import type { Command } from '../commands';
8
8
  import type { TServiceMetas } from './container';
9
- import context from '@server/context';
10
9
  import type { TRouterContext, TAnyRouter } from '../../services/router';
11
10
 
12
11
  export { schema } from '../../services/router/request/validation/zod';
@@ -17,8 +16,7 @@ export type { z } from '../../services/router/request/validation/zod';
17
16
  ----------------------------------*/
18
17
 
19
18
  export type AnyService = Service<{}, {}, Application, any>;
20
-
21
- export type { TRegisteredServicesIndex, TRegisteredService } from './container';
19
+ export type AnyServiceClass = ClassType<AnyService>;
22
20
 
23
21
  /*----------------------------------
24
22
  - TYPES: HOOKS
@@ -40,6 +38,10 @@ type TServiceRouter<TApplication extends Application> = TApplication extends { R
40
38
  : TAnyRouter
41
39
  : TAnyRouter;
42
40
 
41
+ /**
42
+ * @deprecated Services should not depend on request context.
43
+ * Resolve auth/input/request data in controllers and pass explicit typed values into services instead.
44
+ */
43
45
  export type TServiceRequestContext<TApplication extends Application = Application> = TRouterContext<
44
46
  TServiceRouter<TApplication>
45
47
  >;
@@ -59,7 +61,7 @@ export type TSetupConfig<TConfig> = TConfig extends (...args: any[]) => any
59
61
  : TConfig extends Array<infer TItem>
60
62
  ? Array<TSetupConfig<TItem>>
61
63
  : TConfig extends object
62
- ? ({ [K in keyof TConfig]?: TSetupConfig<TConfig[K]> } & Record<string, unknown>)
64
+ ? { [K in keyof TConfig]?: TSetupConfig<TConfig[K]> }
63
65
  : TConfig;
64
66
 
65
67
  export type TServiceArgs<TService extends { config: any; app: any; parent: any }> = [
@@ -133,18 +135,6 @@ export default abstract class Service<
133
135
  return models;
134
136
  }
135
137
 
136
- protected get request(): TServiceRequestContext<TApplication> {
137
- const store = context.getStore() as { requestContext?: TServiceRequestContext<TApplication> } | undefined;
138
- const requestContext = store?.requestContext;
139
-
140
- if (!requestContext)
141
- throw new Error(
142
- `${this.constructor.name} tried to access request context outside of a controller request.`,
143
- );
144
-
145
- return requestContext;
146
- }
147
-
148
138
  /*----------------------------------
149
139
  - LIFECYCLE
150
140
  ----------------------------------*/
@@ -162,12 +152,12 @@ export default abstract class Service<
162
152
  useOptions: { optional?: boolean } = {},
163
153
  ): TService | undefined {
164
154
  const app = this.app as {
165
- registered?: Record<string, { name: string }>;
155
+ findService?: (serviceId: string) => AnyService | undefined;
166
156
  } & Record<string, unknown>;
167
- const registeredService = app.registered?.[serviceId];
168
- if (registeredService !== undefined) return app[registeredService.name] as TService;
157
+ const service = app.findService?.(serviceId);
158
+ if (service !== undefined) return service as TService;
169
159
 
170
- if (useOptions.optional === false) throw new Error(`Service ${registeredService} not registered.`);
160
+ if (useOptions.optional === false) throw new Error(`Service ${serviceId} not registered.`);
171
161
 
172
162
  return undefined;
173
163
  }
@@ -5,7 +5,14 @@
5
5
  "baseUrl": "..",
6
6
  "paths": {
7
7
 
8
- "@/server/models": ["./server/.generated/models.ts"],
8
+ "@/server/models": ["./.proteum/server/models.ts"],
9
+ "@/client/context": ["./.proteum/client/context.ts"],
10
+ "@generated/client/*": ["./.proteum/client/*"],
11
+ "@generated/common/*": ["./.proteum/common/*"],
12
+ "@generated/server/*": ["./.proteum/server/*"],
13
+ "@/client/.generated/*": ["./.proteum/client/*"],
14
+ "@/common/.generated/*": ["./.proteum/common/*"],
15
+ "@/server/.generated/*": ["./.proteum/server/*"],
9
16
 
10
17
  "@client/*": ["../node_modules/proteum/client/*"],
11
18
  "@common/*": ["../node_modules/proteum/common/*"],
@@ -25,4 +32,4 @@
25
32
  ".",
26
33
  "../../node_modules/proteum/types/global"
27
34
  ]
28
- }
35
+ }
package/server/index.ts CHANGED
@@ -1,8 +1,35 @@
1
1
  import AppContainer from './app/container';
2
- import Application from '@/server/.generated/app';
2
+ import Application from '@/server/index';
3
3
  import { isServerHotReloadRequest, serverHotReloadMessageType } from '@common/dev/serverHotReload';
4
4
 
5
5
  const application = AppContainer.start(Application);
6
+ let shutdownPromise: Promise<void> | undefined;
7
+
8
+ const shutdownApplication = async (reason: string) => {
9
+ if (!shutdownPromise) {
10
+ shutdownPromise = (async () => {
11
+ try {
12
+ console.info(`[server] Shutting down (${reason}) ...`);
13
+ await application.runHook('cleanup');
14
+ } catch (error) {
15
+ console.error('[server] Failed to run application cleanup.', error);
16
+ process.exit(1);
17
+ }
18
+
19
+ process.exit(0);
20
+ })();
21
+ }
22
+
23
+ return shutdownPromise;
24
+ };
25
+
26
+ process.once('SIGINT', () => {
27
+ void shutdownApplication('SIGINT');
28
+ });
29
+
30
+ process.once('SIGTERM', () => {
31
+ void shutdownApplication('SIGTERM');
32
+ });
6
33
 
7
34
  if (__DEV__ && typeof process.send === 'function') {
8
35
  process.on('message', (message: unknown) => {
@@ -25,4 +52,8 @@ if (__DEV__ && typeof process.send === 'function') {
25
52
  }
26
53
  })();
27
54
  });
55
+
56
+ process.on('disconnect', () => {
57
+ void shutdownApplication('parent disconnect');
58
+ });
28
59
  }
@@ -11,7 +11,7 @@ import type http from 'http';
11
11
  import type { Application } from '@server/app/index';
12
12
  import Service from '@server/app/service';
13
13
  import { type TAnyRouter, Request as ServerRequest } from '@server/services/router';
14
- import { InputError, AuthRequired, Forbidden } from '@common/errors';
14
+ import * as AuthErrors from '@common/errors';
15
15
 
16
16
  /*----------------------------------
17
17
  - TYPES
@@ -34,6 +34,24 @@ declare global {
34
34
  */
35
35
  interface ProteumAuthFeatureCatalog {}
36
36
 
37
+ /**
38
+ * App-level rule catalog consumed by `auth.check({ ... })`.
39
+ *
40
+ * Apps can extend this interface with their own condition inputs:
41
+ * `interface ProteumAuthRuleCatalog { hasFeature: MyFeatureName }`
42
+ */
43
+ interface ProteumAuthRuleCatalog {
44
+ role: TUserRole;
45
+ }
46
+
47
+ /**
48
+ * Optional tracking context attached to auth / upgrade prompts.
49
+ *
50
+ * Apps can extend this interface with their own machine-readable payload:
51
+ * `interface ProteumAuthTrackingContext extends Partial<BlockedAttempt> {}`
52
+ */
53
+ interface ProteumAuthTrackingContext {}
54
+
37
55
  /**
38
56
  * Canonical feature keys union used across app + framework.
39
57
  *
@@ -52,6 +70,44 @@ export type THttpRequest = express.Request | http.IncomingMessage;
52
70
 
53
71
  export type TFeatureKey = FeatureKeys;
54
72
 
73
+ export type TAuthRuleOutcome = true | false | Error;
74
+
75
+ declare const ProteumAuthRuleNoInputBrand: unique symbol;
76
+
77
+ export type TAuthRuleNoInput = {
78
+ readonly [ProteumAuthRuleNoInputBrand]: 'ProteumAuthRuleNoInput';
79
+ };
80
+
81
+ type TAuthRuleConditionValue<TValue> = TValue extends TAuthRuleNoInput ? true : TValue;
82
+
83
+ type TAuthRuleHandler<TValue> = TValue extends TAuthRuleNoInput
84
+ ? () => TAuthRuleOutcome
85
+ : TValue extends readonly [...infer TArgs]
86
+ ? (...args: TArgs) => TAuthRuleOutcome
87
+ : (input: TValue) => TAuthRuleOutcome;
88
+
89
+ export type TAuthCheckConditions = {
90
+ [TRuleName in Extract<keyof ProteumAuthRuleCatalog, string>]?: TAuthRuleConditionValue<
91
+ ProteumAuthRuleCatalog[TRuleName]
92
+ >;
93
+ };
94
+
95
+ export type TAuthCheckInput = TUserRole | boolean | TAuthCheckConditions | null;
96
+
97
+ export type TAuthConfiguredRules = {
98
+ [TRuleName in Extract<keyof ProteumAuthRuleCatalog, string>]?: TAuthRuleHandler<
99
+ ProteumAuthRuleCatalog[TRuleName]
100
+ >;
101
+ };
102
+
103
+ export type TAuthTrackingContext = ProteumAuthTrackingContext | null;
104
+
105
+ export type TAuthRulesFactory<TUser extends TBasicUser, TRequest extends ServerRequest<TAnyRouter>> = (
106
+ user: TUser,
107
+ tracking: TAuthTrackingContext,
108
+ request: TRequest,
109
+ ) => TAuthConfiguredRules;
110
+
55
111
  /*----------------------------------
56
112
  - CONFIG
57
113
  ----------------------------------*/
@@ -64,7 +120,10 @@ export const UserRoles = ['USER', 'ADMIN', 'TEST', 'DEV'] as const;
64
120
  - SERVICE CONVIG
65
121
  ----------------------------------*/
66
122
 
67
- export type TConfig = {
123
+ export type TConfig<
124
+ TUser extends TBasicUser = TBasicUser,
125
+ TRequest extends ServerRequest<TAnyRouter> = ServerRequest<TAnyRouter>,
126
+ > = {
68
127
  debug: boolean;
69
128
  logoutUrl: string;
70
129
  jwt: {
@@ -72,6 +131,8 @@ export type TConfig = {
72
131
  key: string;
73
132
  expiration: number;
74
133
  };
134
+ unauthenticated?: (tracking: TAuthTrackingContext, request: TRequest) => Error;
135
+ rules?: TAuthRulesFactory<TUser, TRequest>;
75
136
  };
76
137
 
77
138
  export type THooks = {};
@@ -96,7 +157,7 @@ export default abstract class AuthService<
96
157
  TApplication extends Application,
97
158
  TJwtSession extends TBasicJwtSession = TBasicJwtSession,
98
159
  TRequest extends ServerRequest<TAnyRouter> = ServerRequest<TAnyRouter>,
99
- > extends Service<TConfig, THooks, TApplication, TApplication> {
160
+ > extends Service<TConfig<TUser, TRequest>, THooks, TApplication, TApplication> {
100
161
  public login?(request: TRequest, email: string): Promise<unknown>;
101
162
  public abstract decodeSession(jwt: TJwtSession, req: THttpRequest): Promise<TUser | null>;
102
163
 
@@ -196,33 +257,144 @@ export default abstract class AuthService<
196
257
  request.res.clearCookie('authorization');
197
258
  }
198
259
 
199
- public check(request: TRequest, role?: TUserRole | boolean): TUser | null;
260
+ protected getDecodedUser(request: TRequest): TUser | null {
261
+ const user = request.user;
200
262
 
201
- public check(request: TRequest, role: TUserRole | boolean, feature: FeatureKeys, action?: string): TUser | null;
263
+ if (user === undefined) throw new Error(`request.user has not been decoded.`);
202
264
 
203
- public check(
265
+ return user as TUser | null;
266
+ }
267
+
268
+ private resolveErrorTrackingContext(
269
+ tracking: TAuthTrackingContext,
270
+ fallbackFeature: FeatureKeys | null,
271
+ fallbackAction: string,
272
+ ): {
273
+ feature: FeatureKeys;
274
+ action: string;
275
+ details?: {
276
+ data: Exclude<TAuthTrackingContext, null>;
277
+ };
278
+ } {
279
+ const trackingDetails = tracking
280
+ ? (tracking as {
281
+ feature?: string | null;
282
+ action?: string | null;
283
+ })
284
+ : null;
285
+
286
+ const feature =
287
+ typeof trackingDetails?.feature === 'string' && trackingDetails.feature.trim()
288
+ ? (trackingDetails.feature as FeatureKeys)
289
+ : fallbackFeature;
290
+
291
+ if (!feature) throw new AuthErrors.InputError(`This auth rule requires a tracking context with a feature.`);
292
+
293
+ const action =
294
+ typeof trackingDetails?.action === 'string' && trackingDetails.action.trim()
295
+ ? trackingDetails.action
296
+ : fallbackAction;
297
+
298
+ return {
299
+ feature,
300
+ action,
301
+ details: tracking ? { data: tracking as Exclude<TAuthTrackingContext, null> } : undefined,
302
+ };
303
+ }
304
+
305
+ protected buildUnauthenticatedError(request: TRequest, tracking: TAuthTrackingContext): Error {
306
+ if (this.config.unauthenticated) return this.config.unauthenticated(tracking, request);
307
+
308
+ const resolved = this.resolveErrorTrackingContext(tracking, 'auth' as FeatureKeys, 'view');
309
+
310
+ return new AuthErrors.AuthRequired(
311
+ 'Please login to continue',
312
+ resolved.feature,
313
+ resolved.action,
314
+ resolved.details,
315
+ );
316
+ }
317
+
318
+ private isCheckConditions(
319
+ value: TUserRole | boolean | TAuthCheckConditions | null,
320
+ ): value is TAuthCheckConditions {
321
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
322
+ }
323
+
324
+ private invokeConfiguredRule<TRuleName extends Extract<keyof TAuthConfiguredRules, string>>(
325
+ ruleName: TRuleName,
326
+ rule: TAuthConfiguredRules[TRuleName],
327
+ input: TAuthCheckConditions[TRuleName],
328
+ ): TAuthRuleOutcome {
329
+ if (typeof rule !== 'function') throw new AuthErrors.InputError(`Unknown auth rule "${ruleName}".`);
330
+
331
+ const callable = rule as Function;
332
+
333
+ if (callable.length === 0) return callable() as TAuthRuleOutcome;
334
+ if (Array.isArray(input)) return Reflect.apply(callable, undefined, input) as TAuthRuleOutcome;
335
+ return Reflect.apply(callable, undefined, [input]) as TAuthRuleOutcome;
336
+ }
337
+
338
+ private checkWithConditions(
339
+ request: TRequest,
340
+ conditions: TAuthCheckConditions | null | false,
341
+ tracking: TAuthTrackingContext,
342
+ ): TUser | null {
343
+ const user = this.getDecodedUser(request);
344
+
345
+ this.config.debug && console.warn(LogPrefix, `Check auth with rules. Current user =`, user?.name, conditions);
346
+
347
+ if (conditions === false) return user;
348
+
349
+ if (user === null) {
350
+ console.warn(LogPrefix, 'Refusé pour anonyme (' + request.ip + ')');
351
+ throw this.buildUnauthenticatedError(request, tracking);
352
+ }
353
+
354
+ if (!conditions) return user;
355
+
356
+ if (!this.config.rules) throw new AuthErrors.InputError(`Auth rules are not configured for this application.`);
357
+
358
+ const rules = this.config.rules(user, tracking, request);
359
+ const conditionRuleNames = Object.keys(conditions) as Array<Extract<keyof TAuthConfiguredRules, string>>;
360
+
361
+ for (const ruleName of conditionRuleNames) {
362
+ const input = conditions[ruleName];
363
+ if (input === undefined) continue;
364
+
365
+ const outcome = this.invokeConfiguredRule(ruleName, rules[ruleName], input);
366
+ if (outcome === true) continue;
367
+ if (outcome === false)
368
+ throw new AuthErrors.Forbidden('You do not have sufficient permissions to access this resource.');
369
+ throw outcome;
370
+ }
371
+
372
+ return user;
373
+ }
374
+
375
+ protected checkLegacyRole(
204
376
  request: TRequest,
205
377
  role: TUserRole | boolean = 'USER',
206
- feature?: FeatureKeys,
378
+ feature?: FeatureKeys | null,
207
379
  action?: string,
208
380
  ): TUser | null {
209
381
  const normalizedRole = role === true ? 'USER' : role;
210
- const user = request.user;
382
+ const user = this.getDecodedUser(request);
211
383
 
212
384
  this.config.debug &&
213
385
  console.warn(LogPrefix, `Check auth, role = ${normalizedRole}. Current user =`, user?.name, feature);
214
386
 
215
- if (user === undefined) {
216
- throw new Error(`request.user has not been decoded.`);
217
-
218
- // Shoudln't be logged
219
- } else if (normalizedRole === false) {
387
+ if (normalizedRole === false) {
220
388
  return user as TUser;
221
389
 
222
390
  // Not connected
223
391
  } else if (user === null) {
224
392
  console.warn(LogPrefix, 'Refusé pour anonyme (' + request.ip + ')');
225
- throw new AuthRequired('Please login to continue', feature as any, action as any);
393
+ throw new AuthErrors.AuthRequired(
394
+ 'Please login to continue',
395
+ feature && feature !== null ? feature : ('auth' as FeatureKeys),
396
+ action || 'view',
397
+ );
226
398
 
227
399
  // Insufficient permissions
228
400
  } else if (!user.roles.includes(normalizedRole)) {
@@ -231,7 +403,7 @@ export default abstract class AuthService<
231
403
  'Refusé: ' + normalizedRole + ' pour ' + user.name + ' (' + (user.roles || 'role inconnu') + ')',
232
404
  );
233
405
 
234
- throw new Forbidden('You do not have sufficient permissions to access this resource.');
406
+ throw new AuthErrors.Forbidden('You do not have sufficient permissions to access this resource.');
235
407
  } else {
236
408
  this.config.debug &&
237
409
  console.warn(
@@ -242,4 +414,51 @@ export default abstract class AuthService<
242
414
 
243
415
  return user as TUser;
244
416
  }
417
+
418
+ public check(request: TRequest): TUser;
419
+
420
+ public check(request: TRequest, conditions: null, tracking?: TAuthTrackingContext): TUser;
421
+
422
+ public check(request: TRequest, conditions: TAuthCheckConditions, tracking?: TAuthTrackingContext): TUser;
423
+
424
+ public check(request: TRequest, conditions: false, tracking?: TAuthTrackingContext): null;
425
+
426
+ public check(request: TRequest, role?: TUserRole | boolean): TUser | null;
427
+
428
+ public check(request: TRequest, role: TUserRole | boolean, feature: FeatureKeys, action?: string): TUser | null;
429
+
430
+ public check(
431
+ request: TRequest,
432
+ roleOrConditions: TUserRole | boolean | TAuthCheckConditions | null = null,
433
+ featureOrTracking?: FeatureKeys | null | TAuthTrackingContext,
434
+ action?: string,
435
+ ): TUser | null {
436
+ if (roleOrConditions === null || this.isCheckConditions(roleOrConditions)) {
437
+ const tracking = (featureOrTracking ?? null) as TAuthTrackingContext;
438
+ return this.checkWithConditions(request, roleOrConditions, tracking);
439
+ }
440
+
441
+ if (roleOrConditions === false) {
442
+ if (
443
+ featureOrTracking === undefined ||
444
+ featureOrTracking === null ||
445
+ typeof featureOrTracking === 'object'
446
+ ) {
447
+ const tracking = (featureOrTracking ?? null) as TAuthTrackingContext;
448
+ return this.checkWithConditions(request, false, tracking);
449
+ }
450
+
451
+ return this.checkLegacyRole(request, false, featureOrTracking, action);
452
+ }
453
+
454
+ if ((roleOrConditions === true || typeof roleOrConditions === 'string') && this.config.rules) {
455
+ return this.checkWithConditions(
456
+ request,
457
+ { role: roleOrConditions === true ? 'USER' : roleOrConditions },
458
+ null,
459
+ );
460
+ }
461
+
462
+ return this.checkLegacyRole(request, roleOrConditions, featureOrTracking as FeatureKeys | null | undefined, action);
463
+ }
245
464
  }
@@ -18,7 +18,7 @@ import type { Application } from '@server/app/index';
18
18
  import type { TRouterServiceArgs } from '@server/services/router/service';
19
19
 
20
20
  // Specific
21
- import type { default as UsersService, TUserRole, TBasicUser } from '..';
21
+ import type { default as UsersService, TAuthCheckConditions, TBasicUser } from '..';
22
22
  import UsersRequestService from './request';
23
23
 
24
24
  /*----------------------------------
@@ -71,13 +71,45 @@ export default class AuthenticationRouterService<
71
71
  // Check route permissions
72
72
  this.parent.on('resolved', async (route: TAnyRoute, request: TRequest, response: ServerResponse<TRouter>) => {
73
73
  if (route.options.auth !== undefined) {
74
- // Basic auth check
75
- const requiredRole = route.options.auth === true ? 'USER' : route.options.auth;
76
- this.users.check(request, requiredRole as TUserRole | false);
74
+ const tracking = route.options.authTracking ?? null;
77
75
 
78
- // Redirect to logged page
79
- if (route.options.auth === false && request.user && route.options.redirectLogged)
80
- response.redirect(route.options.redirectLogged);
76
+ // Guest-only routes can still redirect authenticated users away.
77
+ if (route.options.auth === false) {
78
+ const currentUser = this.users.check(request, false, tracking);
79
+
80
+ if (route.options.redirectLogged && currentUser) response.redirect(route.options.redirectLogged);
81
+ return;
82
+ }
83
+
84
+ if (route.options.auth === null) {
85
+ this.users.check(request, null, tracking);
86
+ return;
87
+ }
88
+
89
+ if (typeof route.options.auth === 'object') {
90
+ this.users.check(request, route.options.auth as TAuthCheckConditions, tracking);
91
+ return;
92
+ }
93
+
94
+ // `true` keeps the historical "USER" meaning. Use `null` for "logged in only".
95
+ if (route.options.auth === true) {
96
+ if (tracking !== null && this.users.config.rules) {
97
+ this.users.check(request, { role: 'USER' }, tracking);
98
+ return;
99
+ }
100
+
101
+ this.users.check(request, true);
102
+ return;
103
+ }
104
+
105
+ const requiredRole = route.options.auth;
106
+
107
+ if (tracking !== null && this.users.config.rules) {
108
+ this.users.check(request, { role: requiredRole }, tracking);
109
+ return;
110
+ }
111
+
112
+ this.users.check(request, requiredRole);
81
113
  }
82
114
  });
83
115
  }
@@ -2,16 +2,13 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
- // Npm
6
- import jwt from 'jsonwebtoken';
7
-
8
5
  // Core
9
6
  import type { Request as ServerRequest, TAnyRouter } from '@server/services/router';
10
7
  import RequestService from '@server/services/router/request/service';
11
8
 
12
9
  // Specific
13
10
  import type AuthenticationRouterService from '.';
14
- import type { default as UsersManagementService, TUserRole } from '..';
11
+ import type { TAuthCheckConditions, TAuthTrackingContext, TUserRole } from '..';
15
12
 
16
13
  // Types
17
14
  import type { TBasicUser } from '@server/services/auth';
@@ -45,19 +42,54 @@ export default class UsersRequestService<
45
42
  return this.users.logout(this.request);
46
43
  }
47
44
 
45
+ private isCheckConditions(value: TUserRole | true | false | TAuthCheckConditions | null): value is TAuthCheckConditions {
46
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
47
+ }
48
+
48
49
  public check(): TUser;
49
50
 
51
+ public check(conditions: null, tracking?: TAuthTrackingContext): TUser;
52
+
53
+ public check(conditions: TAuthCheckConditions, tracking?: TAuthTrackingContext): TUser;
54
+
55
+ public check(conditions: false, tracking?: TAuthTrackingContext): null;
56
+
50
57
  // TODO: return user type according to entity
51
58
  public check(role: TUserRole, feature: null): TUser;
52
59
 
53
60
  public check(role: false): null;
54
61
 
55
- public check(role: TUserRole, feature: FeatureKeys, action?: string): TUser;
62
+ public check(role: TUserRole | true, feature: FeatureKeys, action?: string): TUser;
56
63
 
57
64
  public check(role: false, feature: FeatureKeys, action?: string): null;
58
65
 
59
- public check(role: TUserRole | false = 'USER', feature?: FeatureKeys | null, action?: string) {
60
- if (feature === null || feature === undefined) return this.users.check(this.request, role);
61
- return this.users.check(this.request, role, feature, action);
66
+ public check(
67
+ roleOrConditions: TUserRole | true | false | TAuthCheckConditions | null = null,
68
+ featureOrTracking?: FeatureKeys | null | TAuthTrackingContext,
69
+ action?: string,
70
+ ) {
71
+ if (roleOrConditions === null) {
72
+ return this.users.check(this.request, null, (featureOrTracking ?? null) as TAuthTrackingContext);
73
+ }
74
+
75
+ if (this.isCheckConditions(roleOrConditions)) {
76
+ return this.users.check(this.request, roleOrConditions, (featureOrTracking ?? null) as TAuthTrackingContext);
77
+ }
78
+
79
+ if (roleOrConditions === false) {
80
+ if (
81
+ featureOrTracking === undefined ||
82
+ featureOrTracking === null ||
83
+ typeof featureOrTracking === 'object'
84
+ ) {
85
+ return this.users.check(this.request, false, (featureOrTracking ?? null) as TAuthTrackingContext);
86
+ }
87
+
88
+ return this.users.check(this.request, false, featureOrTracking, action);
89
+ }
90
+
91
+ if (featureOrTracking === null || featureOrTracking === undefined || typeof featureOrTracking === 'object')
92
+ return this.users.check(this.request, roleOrConditions);
93
+ return this.users.check(this.request, roleOrConditions, featureOrTracking, action);
62
94
  }
63
95
  }