proteum 2.1.2 → 2.1.6
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 +22 -14
- package/README.md +112 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +26 -11
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +17 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +90 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +136 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +193 -6
- package/cli/scaffold/index.ts +14 -25
- package/cli/scaffold/templates.ts +41 -27
- package/cli/utils/agents.ts +4 -2
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2798 -417
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +56 -1
- package/common/dev/session.ts +24 -0
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +3 -1
- package/server/app/container/trace/index.ts +153 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +352 -0
- package/server/services/router/index.ts +50 -47
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +24 -1
- package/server/services/router/response/page/document.tsx +5 -0
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- package/server/services/security/encrypt/aes/service.json +0 -6
|
@@ -22,9 +22,19 @@ import cookieParser from 'cookie-parser';
|
|
|
22
22
|
import Container from '@server/app/container';
|
|
23
23
|
import type CronManager from '@server/services/cron';
|
|
24
24
|
import type CronTask from '@server/services/cron/CronTask';
|
|
25
|
+
import type { TBasicUser } from '@server/services/auth';
|
|
25
26
|
import type { TServerRouter } from '..';
|
|
27
|
+
import type { TDevConsoleLogLevel } from '@common/dev/console';
|
|
28
|
+
import type { TPerfGroupBy } from '@common/dev/performance';
|
|
29
|
+
import type { TDevSessionStartResponse, TDevSessionUserSummary } from '@common/dev/session';
|
|
26
30
|
import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
|
|
27
31
|
import { explainSectionNames } from '@common/dev/diagnostics';
|
|
32
|
+
import {
|
|
33
|
+
connectedProjectHealthPath,
|
|
34
|
+
connectedProjectProxyPathPrefix,
|
|
35
|
+
parseConnectedProjectProxyPath,
|
|
36
|
+
} from '@common/connectedProjects';
|
|
37
|
+
import { profilerTraceRequestIdHeader } from '@common/dev/profiler';
|
|
28
38
|
|
|
29
39
|
// Middlewaees (core)
|
|
30
40
|
import { isMutipart, MiddlewareFormData } from './multipart';
|
|
@@ -54,6 +64,15 @@ export type Hooks = {};
|
|
|
54
64
|
|
|
55
65
|
type TContentSecurityPolicyOptions = NonNullable<Parameters<typeof helmet.contentSecurityPolicy>[0]>;
|
|
56
66
|
type TContentSecurityPolicyDirectives = NonNullable<TContentSecurityPolicyOptions['directives']>;
|
|
67
|
+
type TDevSessionAuthService = {
|
|
68
|
+
config: {
|
|
69
|
+
jwt: {
|
|
70
|
+
expiration: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
createSession: (session: { email: string }, request: { id: string; res: express.Response }) => string;
|
|
74
|
+
decodeSession: (session: { email: string }, req: express.Request) => Promise<TBasicUser | null>;
|
|
75
|
+
};
|
|
57
76
|
|
|
58
77
|
const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPolicyOptions => {
|
|
59
78
|
const directives: TContentSecurityPolicyDirectives = {
|
|
@@ -101,6 +120,153 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
101
120
|
this.app.on('cleanup', () => this.cleanup());
|
|
102
121
|
}
|
|
103
122
|
|
|
123
|
+
private resolveDevSessionAuthService(): TDevSessionAuthService {
|
|
124
|
+
const plugins = Object.values(this.router.config.plugins || {}) as Array<{ users?: unknown }>;
|
|
125
|
+
|
|
126
|
+
for (const plugin of plugins) {
|
|
127
|
+
const users = plugin?.users as Partial<TDevSessionAuthService> | undefined;
|
|
128
|
+
if (!users) continue;
|
|
129
|
+
if (typeof users.createSession !== 'function') continue;
|
|
130
|
+
if (typeof users.decodeSession !== 'function') continue;
|
|
131
|
+
if (typeof users.config?.jwt?.expiration !== 'number') continue;
|
|
132
|
+
|
|
133
|
+
return users as TDevSessionAuthService;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error('No auth router plugin with a compatible users service is registered.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private summarizeDevSessionUser(user: TBasicUser): TDevSessionUserSummary {
|
|
140
|
+
return {
|
|
141
|
+
email: user.email,
|
|
142
|
+
name: user.name,
|
|
143
|
+
type: user.type,
|
|
144
|
+
roles: [...user.roles],
|
|
145
|
+
locale: user.locale ?? null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async verifyConnectedProjectsBeforeStart() {
|
|
150
|
+
for (const connectedProject of Object.values(this.app.connectedProjects || {})) {
|
|
151
|
+
const healthUrl = new URL(connectedProjectHealthPath, connectedProject.urlInternal).toString();
|
|
152
|
+
|
|
153
|
+
let response: Response;
|
|
154
|
+
try {
|
|
155
|
+
response = await fetch(healthUrl, {
|
|
156
|
+
headers: { Accept: 'application/json' },
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Connected project "${connectedProject.namespace}" is unreachable at ${connectedProject.urlInternal}. ${error instanceof Error ? error.message : String(error)}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Connected project "${connectedProject.namespace}" health check failed at ${healthUrl} with status ${response.status}.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private registerConnectedProjectRoutes(routes: express.Express) {
|
|
173
|
+
routes.get(connectedProjectHealthPath, (_req, res) => {
|
|
174
|
+
res.json({
|
|
175
|
+
connectedProjects: Object.keys(this.app.connectedProjects || {}),
|
|
176
|
+
identifier: this.app.identity.identifier,
|
|
177
|
+
ok: true,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
routes.all(`${connectedProjectHealthPath}/*`, (_req, res) => {
|
|
182
|
+
res.status(404).json({ error: 'Unknown Proteum connected-project route.' });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
routes.all(`${connectedProjectProxyPathPrefix}/:namespace/*`, async (req, res, next) => {
|
|
186
|
+
const parsed = parseConnectedProjectProxyPath(req.path);
|
|
187
|
+
if (!parsed) {
|
|
188
|
+
next();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const connectedProject = this.app.connectedProjects?.[parsed.namespace];
|
|
193
|
+
if (!connectedProject) {
|
|
194
|
+
res.status(404).json({ error: `Unknown connected project "${parsed.namespace}".` });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const search = new URL(req.originalUrl, 'http://proteum.local').search;
|
|
199
|
+
const targetUrl = new URL(`${parsed.httpPath}${search}`, connectedProject.urlInternal).toString();
|
|
200
|
+
const headers = new Headers();
|
|
201
|
+
|
|
202
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
203
|
+
if (!value) continue;
|
|
204
|
+
if (key === 'content-length' || key === 'host') continue;
|
|
205
|
+
headers.set(key, Array.isArray(value) ? value.join(', ') : String(value));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!headers.has('accept')) headers.set('accept', 'application/json');
|
|
209
|
+
|
|
210
|
+
const init: RequestInit = {
|
|
211
|
+
method: req.method,
|
|
212
|
+
headers,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
216
|
+
if (req.files && Object.keys(req.files).length > 0) {
|
|
217
|
+
res.status(501).json({
|
|
218
|
+
error: `Connected project proxy does not support multipart payloads for ${parsed.namespace} yet.`,
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const contentType = String(req.headers['content-type'] || '').toLowerCase();
|
|
224
|
+
|
|
225
|
+
if (contentType.includes('application/json')) {
|
|
226
|
+
headers.set('content-type', 'application/json');
|
|
227
|
+
init.body = JSON.stringify(req.body || {});
|
|
228
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
229
|
+
headers.set('content-type', 'application/x-www-form-urlencoded');
|
|
230
|
+
init.body = new URLSearchParams(req.body as Record<string, string>).toString();
|
|
231
|
+
} else {
|
|
232
|
+
headers.delete('content-type');
|
|
233
|
+
init.body = undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let response: Response;
|
|
238
|
+
try {
|
|
239
|
+
response = await fetch(targetUrl, init);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
res.status(502).json({
|
|
242
|
+
error: `Failed to proxy connected request to ${connectedProject.namespace}. ${error instanceof Error ? error.message : String(error)}`,
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const traceRequestId = response.headers.get(profilerTraceRequestIdHeader);
|
|
248
|
+
if (traceRequestId) res.setHeader(profilerTraceRequestIdHeader, traceRequestId);
|
|
249
|
+
|
|
250
|
+
const setCookie = typeof (response.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie === 'function'
|
|
251
|
+
? (response.headers as Headers & { getSetCookie: () => string[] }).getSetCookie()
|
|
252
|
+
: [];
|
|
253
|
+
if (setCookie.length > 0) {
|
|
254
|
+
res.setHeader('set-cookie', setCookie);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const contentType = response.headers.get('content-type') || '';
|
|
258
|
+
res.status(response.status);
|
|
259
|
+
if (contentType) res.setHeader('content-type', contentType);
|
|
260
|
+
|
|
261
|
+
if (contentType.includes('application/json')) {
|
|
262
|
+
res.json(await response.json());
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
res.send(await response.text());
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
104
270
|
/*----------------------------------
|
|
105
271
|
- HOOKS
|
|
106
272
|
----------------------------------*/
|
|
@@ -143,6 +309,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
143
309
|
);
|
|
144
310
|
routes.use(apiMultipartOnly(MiddlewareFormData));
|
|
145
311
|
if (this.config.cors !== undefined) routes.use(apiOnly(cors(this.config.cors)));
|
|
312
|
+
this.registerConnectedProjectRoutes(routes);
|
|
146
313
|
routes.use(apiOnly(routeRequest));
|
|
147
314
|
|
|
148
315
|
// Diverses protections (dont le disable x-powered-by)
|
|
@@ -231,6 +398,8 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
231
398
|
/*----------------------------------
|
|
232
399
|
- BOOT SERVICES
|
|
233
400
|
----------------------------------*/
|
|
401
|
+
await this.verifyConnectedProjectsBeforeStart();
|
|
402
|
+
|
|
234
403
|
this.http.listen(this.config.port, () => {
|
|
235
404
|
if (__DEV__ && typeof process.send === 'function') {
|
|
236
405
|
process.send({
|
|
@@ -311,6 +480,17 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
311
480
|
}
|
|
312
481
|
});
|
|
313
482
|
|
|
483
|
+
routes.get('/__proteum/explain/owner', (req, res) => {
|
|
484
|
+
const query = Array.isArray(req.query.query) ? req.query.query[0] : req.query.query;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
res.json(this.app.getDevDiagnostics().explainOwner(typeof query === 'string' ? query : ''));
|
|
488
|
+
} catch (error) {
|
|
489
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
490
|
+
res.status(message.includes('required') ? 400 : 500).json({ error: message });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
314
494
|
routes.get('/__proteum/doctor', (req, res) => {
|
|
315
495
|
const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
|
|
316
496
|
const strict = rawStrict === '1' || rawStrict === 'true';
|
|
@@ -322,6 +502,129 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
322
502
|
}
|
|
323
503
|
});
|
|
324
504
|
|
|
505
|
+
routes.get('/__proteum/doctor/contracts', (req, res) => {
|
|
506
|
+
const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
|
|
507
|
+
const strict = rawStrict === '1' || rawStrict === 'true';
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
res.json(this.app.getDevDiagnostics().doctorContracts(strict));
|
|
511
|
+
} catch (error) {
|
|
512
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
routes.get('/__proteum/logs', (req, res) => {
|
|
517
|
+
const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
|
|
518
|
+
const rawLevel = Array.isArray(req.query.level) ? req.query.level[0] : req.query.level;
|
|
519
|
+
const limit = Math.max(0, Math.min(500, Number(rawLimit) || 100));
|
|
520
|
+
const level = typeof rawLevel === 'string' ? (rawLevel as TDevConsoleLogLevel) : 'log';
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
res.json(this.app.getDevDiagnostics().readLogs(limit, level));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
routes.get('/__proteum/diagnose', (req, res) => {
|
|
530
|
+
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
531
|
+
const readNumber = (value: unknown, fallback: number) => {
|
|
532
|
+
const parsed = Number(readString(value));
|
|
533
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
res.json(
|
|
538
|
+
this.app.getDevDiagnostics().diagnose({
|
|
539
|
+
logsLevel:
|
|
540
|
+
typeof readString(req.query.logsLevel) === 'string'
|
|
541
|
+
? (readString(req.query.logsLevel) as TDevConsoleLogLevel)
|
|
542
|
+
: 'warn',
|
|
543
|
+
logsLimit: readNumber(req.query.logsLimit, 40),
|
|
544
|
+
path: typeof readString(req.query.path) === 'string' ? readString(req.query.path) : undefined,
|
|
545
|
+
query: typeof readString(req.query.query) === 'string' ? readString(req.query.query) : undefined,
|
|
546
|
+
requestId: typeof readString(req.query.requestId) === 'string' ? readString(req.query.requestId) : undefined,
|
|
547
|
+
strict: readString(req.query.strict) === '1' || readString(req.query.strict) === 'true',
|
|
548
|
+
}),
|
|
549
|
+
);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
552
|
+
res.status(message.includes('required') || message.includes('Diagnose requires') ? 400 : 500).json({ error: message });
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
routes.get('/__proteum/perf/top', (req, res) => {
|
|
557
|
+
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
558
|
+
const readNumber = (value: unknown, fallback: number) => {
|
|
559
|
+
const parsed = Number(readString(value));
|
|
560
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
res.json(
|
|
565
|
+
this.app.getDevDiagnostics().perfTop({
|
|
566
|
+
groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
|
|
567
|
+
limit: readNumber(req.query.limit, 12),
|
|
568
|
+
since: typeof readString(req.query.since) === 'string' ? readString(req.query.since) : undefined,
|
|
569
|
+
}),
|
|
570
|
+
);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
routes.get('/__proteum/perf/compare', (req, res) => {
|
|
577
|
+
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
578
|
+
const readNumber = (value: unknown, fallback: number) => {
|
|
579
|
+
const parsed = Number(readString(value));
|
|
580
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
res.json(
|
|
585
|
+
this.app.getDevDiagnostics().perfCompare({
|
|
586
|
+
baseline: typeof readString(req.query.baseline) === 'string' ? readString(req.query.baseline) : undefined,
|
|
587
|
+
groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
|
|
588
|
+
limit: readNumber(req.query.limit, 12),
|
|
589
|
+
target: typeof readString(req.query.target) === 'string' ? readString(req.query.target) : undefined,
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
routes.get('/__proteum/perf/memory', (req, res) => {
|
|
598
|
+
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
599
|
+
const readNumber = (value: unknown, fallback: number) => {
|
|
600
|
+
const parsed = Number(readString(value));
|
|
601
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
res.json(
|
|
606
|
+
this.app.getDevDiagnostics().perfMemory({
|
|
607
|
+
groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
|
|
608
|
+
limit: readNumber(req.query.limit, 12),
|
|
609
|
+
since: typeof readString(req.query.since) === 'string' ? readString(req.query.since) : undefined,
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
routes.get('/__proteum/perf/request', (req, res) => {
|
|
618
|
+
const query = Array.isArray(req.query.query) ? req.query.query[0] : req.query.query;
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
res.json(this.app.getDevDiagnostics().perfRequest(typeof query === 'string' ? query : ''));
|
|
622
|
+
} catch (error) {
|
|
623
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
624
|
+
res.status(message.includes('Could not find') || message.includes('required') ? 404 : 400).json({ error: message });
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
325
628
|
routes.get('/__proteum/cron/tasks', (_req, res) => {
|
|
326
629
|
const cron = this.getCronManager();
|
|
327
630
|
res.json({
|
|
@@ -358,6 +661,55 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
358
661
|
}
|
|
359
662
|
});
|
|
360
663
|
|
|
664
|
+
routes.post('/__proteum/session/start', async (req, res) => {
|
|
665
|
+
const email = typeof req.body?.email === 'string' ? req.body.email.trim() : '';
|
|
666
|
+
const requiredRole = typeof req.body?.role === 'string' ? req.body.role.trim() : '';
|
|
667
|
+
|
|
668
|
+
if (!email) {
|
|
669
|
+
res.status(400).json({ error: 'Email is required.' });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
const auth = this.resolveDevSessionAuthService();
|
|
675
|
+
const user = await auth.decodeSession({ email }, req);
|
|
676
|
+
|
|
677
|
+
if (!user) {
|
|
678
|
+
res.status(404).json({ error: `No user could be resolved for "${email}".` });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (requiredRole && !user.roles.includes(requiredRole)) {
|
|
683
|
+
res.status(403).json({ error: `User "${email}" does not have required role "${requiredRole}".` });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const token = auth.createSession(
|
|
688
|
+
{ email },
|
|
689
|
+
{
|
|
690
|
+
id: `proteum-session:${Date.now()}`,
|
|
691
|
+
res,
|
|
692
|
+
},
|
|
693
|
+
);
|
|
694
|
+
const issuedAt = new Date().toISOString();
|
|
695
|
+
const expiresAt = new Date(Date.now() + auth.config.jwt.expiration).toISOString();
|
|
696
|
+
const response: TDevSessionStartResponse = {
|
|
697
|
+
user: this.summarizeDevSessionUser(user),
|
|
698
|
+
session: {
|
|
699
|
+
token,
|
|
700
|
+
cookieName: 'authorization',
|
|
701
|
+
expiresInMs: auth.config.jwt.expiration,
|
|
702
|
+
issuedAt,
|
|
703
|
+
expiresAt,
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
res.json(response);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
361
713
|
routes.post('/__proteum/cron/tasks/run', async (req, res) => {
|
|
362
714
|
const cron = this.getCronManager();
|
|
363
715
|
if (!cron) {
|
|
@@ -72,6 +72,8 @@ type TGeneratedRouteModule = { filepath: string; register?: TRouteModule['__regi
|
|
|
72
72
|
|
|
73
73
|
type TGeneratedControllerDefinition = {
|
|
74
74
|
path: string;
|
|
75
|
+
filepath: string;
|
|
76
|
+
sourceLocation: { line: number; column: number };
|
|
75
77
|
Controller: new (request: TRouterContext<TServerRouter>) => { [method: string]: () => any };
|
|
76
78
|
method: string;
|
|
77
79
|
};
|
|
@@ -114,6 +116,7 @@ export type Config<
|
|
|
114
116
|
disk?: string; // Disk driver ID
|
|
115
117
|
|
|
116
118
|
currentDomain: string;
|
|
119
|
+
defaultRouteOptions?: Partial<TRouteOptions>;
|
|
117
120
|
|
|
118
121
|
http: HttpServiceConfig;
|
|
119
122
|
|
|
@@ -353,7 +356,7 @@ export default class ServerRouter<
|
|
|
353
356
|
const controller = new definition.Controller(requestContext);
|
|
354
357
|
return controller[definition.method]();
|
|
355
358
|
},
|
|
356
|
-
options: { ...defaultOptions },
|
|
359
|
+
options: { ...defaultOptions, filepath: definition.filepath, sourceLocation: definition.sourceLocation },
|
|
357
360
|
};
|
|
358
361
|
|
|
359
362
|
this.controllers[route.path] = route;
|
|
@@ -363,6 +366,14 @@ export default class ServerRouter<
|
|
|
363
366
|
public url = (path: string, params: {} = {}, absolute: boolean = true) =>
|
|
364
367
|
buildUrl(path, params, this.config.currentDomain, absolute);
|
|
365
368
|
|
|
369
|
+
private buildRouteOptions(options: Partial<TRouteOptions> = {}): TRouteOptions {
|
|
370
|
+
return {
|
|
371
|
+
...defaultOptions,
|
|
372
|
+
...(this.config.defaultRouteOptions || {}),
|
|
373
|
+
...options,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
366
377
|
/*----------------------------------
|
|
367
378
|
- REGISTER
|
|
368
379
|
----------------------------------*/
|
|
@@ -378,12 +389,11 @@ export default class ServerRouter<
|
|
|
378
389
|
regex,
|
|
379
390
|
keys,
|
|
380
391
|
controller: (context: TRouterContext<this>) => new Page(route, renderer, context, layout),
|
|
381
|
-
options: {
|
|
382
|
-
...defaultOptions,
|
|
392
|
+
options: this.buildRouteOptions({
|
|
383
393
|
accept: 'html', // Les pages retournent forcémment du html
|
|
384
394
|
setup,
|
|
385
395
|
...options,
|
|
386
|
-
},
|
|
396
|
+
}),
|
|
387
397
|
};
|
|
388
398
|
|
|
389
399
|
this.routes.push(route);
|
|
@@ -396,7 +406,7 @@ export default class ServerRouter<
|
|
|
396
406
|
options: Partial<TRouteOptions>,
|
|
397
407
|
renderer: TFrontRenderer<{}, { message: string }>,
|
|
398
408
|
) {
|
|
399
|
-
const finalOptions =
|
|
409
|
+
const finalOptions = this.buildRouteOptions(options);
|
|
400
410
|
|
|
401
411
|
// Automatic layout form the nearest _layout folder
|
|
402
412
|
const layout = getLayout('Error ' + code, finalOptions);
|
|
@@ -454,7 +464,7 @@ export default class ServerRouter<
|
|
|
454
464
|
path: path,
|
|
455
465
|
regex,
|
|
456
466
|
keys: keys,
|
|
457
|
-
options:
|
|
467
|
+
options: this.buildRouteOptions(options),
|
|
458
468
|
controller,
|
|
459
469
|
};
|
|
460
470
|
|
|
@@ -711,6 +721,14 @@ export default class ServerRouter<
|
|
|
711
721
|
channelId: request.id,
|
|
712
722
|
method: request.method,
|
|
713
723
|
path: request.path,
|
|
724
|
+
...(request.traceCall
|
|
725
|
+
? {
|
|
726
|
+
traceCallFetcherId: request.traceCall.fetcherId,
|
|
727
|
+
traceCallId: request.traceCall.id,
|
|
728
|
+
traceCallLabel: request.traceCall.label,
|
|
729
|
+
traceCallOrigin: request.traceCall.origin,
|
|
730
|
+
}
|
|
731
|
+
: {}),
|
|
714
732
|
},
|
|
715
733
|
async () => {
|
|
716
734
|
const timeStart = Date.now();
|
|
@@ -745,6 +763,11 @@ export default class ServerRouter<
|
|
|
745
763
|
path: request.path,
|
|
746
764
|
accept: controllerRoute.options.accept || '',
|
|
747
765
|
filepath: controllerRoute.options.filepath || '',
|
|
766
|
+
source: {
|
|
767
|
+
filepath: controllerRoute.options.filepath || '',
|
|
768
|
+
line: controllerRoute.options.sourceLocation?.line || 0,
|
|
769
|
+
column: controllerRoute.options.sourceLocation?.column || 0,
|
|
770
|
+
},
|
|
748
771
|
},
|
|
749
772
|
'summary',
|
|
750
773
|
);
|
|
@@ -777,6 +800,11 @@ export default class ServerRouter<
|
|
|
777
800
|
routePath: route.path || '',
|
|
778
801
|
routeId: route.options.id || '',
|
|
779
802
|
filepath: route.options.filepath || '',
|
|
803
|
+
source: {
|
|
804
|
+
filepath: route.options.filepath || '',
|
|
805
|
+
line: route.options.sourceLocation?.line || 0,
|
|
806
|
+
column: route.options.sourceLocation?.column || 0,
|
|
807
|
+
},
|
|
780
808
|
},
|
|
781
809
|
'deep',
|
|
782
810
|
);
|
|
@@ -797,6 +825,11 @@ export default class ServerRouter<
|
|
|
797
825
|
routePath: route.path || '',
|
|
798
826
|
routeId: route.options.id || '',
|
|
799
827
|
filepath: route.options.filepath || '',
|
|
828
|
+
source: {
|
|
829
|
+
filepath: route.options.filepath || '',
|
|
830
|
+
line: route.options.sourceLocation?.line || 0,
|
|
831
|
+
column: route.options.sourceLocation?.column || 0,
|
|
832
|
+
},
|
|
800
833
|
},
|
|
801
834
|
'deep',
|
|
802
835
|
);
|
|
@@ -816,6 +849,11 @@ export default class ServerRouter<
|
|
|
816
849
|
routePath: route.path || '',
|
|
817
850
|
routeId: route.options.id || '',
|
|
818
851
|
filepath: route.options.filepath || '',
|
|
852
|
+
source: {
|
|
853
|
+
filepath: route.options.filepath || '',
|
|
854
|
+
line: route.options.sourceLocation?.line || 0,
|
|
855
|
+
column: route.options.sourceLocation?.column || 0,
|
|
856
|
+
},
|
|
819
857
|
},
|
|
820
858
|
'deep',
|
|
821
859
|
);
|
|
@@ -867,6 +905,11 @@ export default class ServerRouter<
|
|
|
867
905
|
routePath: route.path || '',
|
|
868
906
|
routeId: route.options.id || '',
|
|
869
907
|
filepath: route.options.filepath || '',
|
|
908
|
+
source: {
|
|
909
|
+
filepath: route.options.filepath || '',
|
|
910
|
+
line: route.options.sourceLocation?.line || 0,
|
|
911
|
+
column: route.options.sourceLocation?.column || 0,
|
|
912
|
+
},
|
|
870
913
|
accept: route.options.accept || '',
|
|
871
914
|
method: route.method,
|
|
872
915
|
},
|
|
@@ -901,47 +944,7 @@ export default class ServerRouter<
|
|
|
901
944
|
};
|
|
902
945
|
|
|
903
946
|
private async resolveApiBatch(fetchers: TFetcherList, request: ServerRequest<this>) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const responseData: TObjetDonnees = {};
|
|
907
|
-
for (const id in fetchers) {
|
|
908
|
-
const fetcher = fetchers[id];
|
|
909
|
-
if (!fetcher || !('method' in fetcher)) continue;
|
|
910
|
-
|
|
911
|
-
const { method, path, data } = fetcher;
|
|
912
|
-
const callId = this.app.container.Trace.startCall(request.id, {
|
|
913
|
-
origin: 'api-batch-fetcher',
|
|
914
|
-
label: id,
|
|
915
|
-
method,
|
|
916
|
-
path,
|
|
917
|
-
fetcherId: id,
|
|
918
|
-
requestDataKeys: data && typeof data === 'object' ? Object.keys(data) : [],
|
|
919
|
-
requestData: data,
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
try {
|
|
923
|
-
const response = await this.resolve(request.children(method, path, data));
|
|
924
|
-
responseData[id] = response.data;
|
|
925
|
-
this.app.container.Trace.finishCall(request.id, callId, {
|
|
926
|
-
statusCode: response.statusCode,
|
|
927
|
-
resultKeys:
|
|
928
|
-
response.data && typeof response.data === 'object' && !Array.isArray(response.data)
|
|
929
|
-
? Object.keys(response.data as Record<string, unknown>)
|
|
930
|
-
: [],
|
|
931
|
-
result: response.data,
|
|
932
|
-
});
|
|
933
|
-
} catch (error) {
|
|
934
|
-
const typedError = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
|
935
|
-
const statusCode = 'http' in typedError ? Number((typedError as Error & { http?: number }).http) : undefined;
|
|
936
|
-
this.app.container.Trace.finishCall(request.id, callId, {
|
|
937
|
-
statusCode: Number.isFinite(statusCode) ? statusCode : undefined,
|
|
938
|
-
errorMessage: typedError.message,
|
|
939
|
-
});
|
|
940
|
-
throw error;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// TODO: merge response.headers ?
|
|
944
|
-
}
|
|
947
|
+
const responseData = await request.api.fetchSync(fetchers, {});
|
|
945
948
|
|
|
946
949
|
// Status
|
|
947
950
|
request.res.status(200);
|