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.
- package/AGENTS.md +13 -1
- package/README.md +375 -0
- package/agents/framework/AGENTS.md +917 -0
- package/agents/project/AGENTS.md +138 -0
- package/agents/{codex → project}/CODING_STYLE.md +3 -2
- package/agents/project/client/AGENTS.md +108 -0
- package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
- package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
- package/agents/project/server/services/AGENTS.md +170 -0
- package/agents/{codex → project}/tests/AGENTS.md +1 -0
- package/cli/app/config.ts +3 -2
- package/cli/app/index.ts +6 -66
- package/cli/bin.js +7 -2
- package/cli/commands/build.ts +94 -27
- package/cli/commands/check.ts +15 -1
- package/cli/commands/dev.ts +288 -132
- package/cli/commands/doctor.ts +108 -0
- package/cli/commands/explain.ts +226 -0
- package/cli/commands/init.ts +76 -70
- package/cli/commands/lint.ts +18 -1
- package/cli/commands/refresh.ts +16 -6
- package/cli/commands/typecheck.ts +14 -1
- package/cli/compiler/artifacts/controllers.ts +150 -0
- package/cli/compiler/artifacts/discovery.ts +132 -0
- package/cli/compiler/artifacts/manifest.ts +267 -0
- package/cli/compiler/artifacts/routing.ts +315 -0
- package/cli/compiler/artifacts/services.ts +480 -0
- package/cli/compiler/artifacts/shared.ts +12 -0
- package/cli/compiler/client/identite.ts +2 -1
- package/cli/compiler/client/index.ts +13 -3
- package/cli/compiler/common/controllers.ts +23 -28
- package/cli/compiler/common/files/style.ts +3 -4
- package/cli/compiler/common/generatedRouteModules.ts +333 -19
- package/cli/compiler/common/proteumManifest.ts +133 -0
- package/cli/compiler/index.ts +33 -896
- package/cli/compiler/server/index.ts +21 -4
- package/cli/context.ts +71 -0
- package/cli/index.ts +39 -181
- package/cli/presentation/commands.ts +208 -0
- package/cli/presentation/compileReporter.ts +65 -0
- package/cli/presentation/devSession.ts +70 -0
- package/cli/presentation/help.ts +193 -0
- package/cli/presentation/ink.ts +69 -0
- package/cli/presentation/layout.ts +83 -0
- package/cli/runtime/argv.ts +49 -0
- package/cli/runtime/command.ts +25 -0
- package/cli/runtime/commands.ts +221 -0
- package/cli/runtime/importEsm.ts +7 -0
- package/cli/runtime/verbose.ts +15 -0
- package/cli/utils/agents.ts +5 -4
- package/cli/utils/keyboard.ts +12 -6
- package/client/app/index.ts +0 -6
- package/client/services/router/index.tsx +1 -1
- package/client/services/router/response/index.tsx +2 -2
- package/common/dev/serverHotReload.ts +12 -0
- package/common/router/index.ts +3 -2
- package/common/router/layouts.ts +1 -1
- package/common/router/pageSetup.ts +1 -0
- package/package.json +10 -8
- package/prettier/router-registration-plugin.cjs +52 -0
- package/prettier.config.cjs +1 -0
- package/scripts/cleanup-generated-controllers.ts +2 -2
- package/scripts/fix-reference-app-typing.ts +2 -2
- package/scripts/format-router-registrations.ts +119 -0
- package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
- package/scripts/refactor-server-controllers.ts +19 -18
- package/scripts/refactor-server-runtime-aliases.ts +1 -1
- package/server/app/commands.ts +309 -25
- package/server/app/container/config.ts +1 -1
- package/server/app/container/index.ts +2 -2
- package/server/app/controller/index.ts +13 -4
- package/server/app/index.ts +53 -37
- package/server/app/service/container.ts +26 -28
- package/server/app/service/index.ts +10 -20
- package/server/app.tsconfig.json +9 -2
- package/server/index.ts +32 -1
- package/server/services/auth/index.ts +234 -15
- package/server/services/auth/router/index.ts +39 -7
- package/server/services/auth/router/request.ts +40 -8
- package/server/services/disks/index.ts +1 -1
- package/server/services/prisma/Facet.ts +2 -2
- package/server/services/prisma/index.ts +22 -5
- package/server/services/prisma/mariadb.ts +47 -0
- package/server/services/router/http/index.ts +9 -1
- package/server/services/router/index.ts +10 -4
- package/server/services/router/response/index.ts +26 -6
- package/types/auth-check-rules.test.ts +51 -0
- package/types/controller-request-context.test.ts +55 -0
- package/types/service-config.test.ts +39 -0
- package/agents/codex/AGENTS.md +0 -95
- package/agents/codex/client/AGENTS.md +0 -102
- package/agents/codex/server/services/AGENTS.md +0 -137
- package/server/services/models.7z +0 -0
- /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
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
155
|
+
findService?: (serviceId: string) => AnyService | undefined;
|
|
166
156
|
} & Record<string, unknown>;
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
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 ${
|
|
160
|
+
if (useOptions.optional === false) throw new Error(`Service ${serviceId} not registered.`);
|
|
171
161
|
|
|
172
162
|
return undefined;
|
|
173
163
|
}
|
package/server/app.tsconfig.json
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
"baseUrl": "..",
|
|
6
6
|
"paths": {
|
|
7
7
|
|
|
8
|
-
"@/server/models": ["
|
|
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
|
|
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
|
|
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
|
-
|
|
260
|
+
protected getDecodedUser(request: TRequest): TUser | null {
|
|
261
|
+
const user = request.user;
|
|
200
262
|
|
|
201
|
-
|
|
263
|
+
if (user === undefined) throw new Error(`request.user has not been decoded.`);
|
|
202
264
|
|
|
203
|
-
|
|
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
|
|
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 (
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
79
|
-
if (route.options.auth === false
|
|
80
|
-
|
|
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 {
|
|
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(
|
|
60
|
-
|
|
61
|
-
|
|
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
|
}
|