@medyll/idae-api 0.137.0 → 0.139.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/README.md +61 -0
- package/dist/__tests__/README.md +583 -0
- package/dist/__tests__/fixtures/mockData.d.ts +219 -0
- package/dist/__tests__/fixtures/mockData.js +243 -0
- package/dist/__tests__/helpers/testUtils.d.ts +153 -0
- package/dist/__tests__/helpers/testUtils.js +328 -0
- package/dist/client/IdaeApiClient.d.ts +7 -0
- package/dist/client/IdaeApiClient.js +15 -2
- package/dist/client/IdaeApiClientCollection.d.ts +17 -9
- package/dist/client/IdaeApiClientCollection.js +32 -11
- package/dist/client/IdaeApiClientConfig.d.ts +2 -0
- package/dist/client/IdaeApiClientConfig.js +10 -2
- package/dist/client/IdaeApiClientRequest.js +30 -15
- package/dist/config/routeDefinitions.d.ts +8 -0
- package/dist/config/routeDefinitions.js +63 -15
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/openApi/redoc.html +12 -0
- package/dist/openApi/swagger-ui.html +23 -0
- package/dist/server/IdaeApi.d.ts +15 -0
- package/dist/server/IdaeApi.js +94 -22
- package/dist/server/engine/requestDatabaseManager.d.ts +1 -1
- package/dist/server/engine/requestDatabaseManager.js +6 -10
- package/dist/server/engine/routeManager.d.ts +1 -0
- package/dist/server/engine/routeManager.js +33 -17
- package/dist/server/middleware/README.md +46 -0
- package/dist/server/middleware/authMiddleware.d.ts +63 -0
- package/dist/server/middleware/authMiddleware.js +121 -12
- package/dist/server/middleware/authorizationMiddleware.d.ts +18 -0
- package/dist/server/middleware/authorizationMiddleware.js +38 -0
- package/dist/server/middleware/databaseMiddleware.d.ts +6 -1
- package/dist/server/middleware/databaseMiddleware.js +111 -6
- package/dist/server/middleware/docsMiddleware.d.ts +13 -0
- package/dist/server/middleware/docsMiddleware.js +30 -0
- package/dist/server/middleware/healthMiddleware.d.ts +15 -0
- package/dist/server/middleware/healthMiddleware.js +19 -0
- package/dist/server/middleware/mcpMiddleware.d.ts +10 -0
- package/dist/server/middleware/mcpMiddleware.js +14 -0
- package/dist/server/middleware/openApiMiddleware.d.ts +7 -0
- package/dist/server/middleware/openApiMiddleware.js +20 -0
- package/dist/server/middleware/tenantContextMiddleware.d.ts +25 -0
- package/dist/server/middleware/tenantContextMiddleware.js +31 -0
- package/dist/server/middleware/validationLayer.d.ts +8 -0
- package/dist/server/middleware/validationLayer.js +23 -0
- package/dist/server/middleware/validationMiddleware.d.ts +16 -0
- package/dist/server/middleware/validationMiddleware.js +30 -0
- package/package.json +18 -4
|
@@ -1,15 +1,78 @@
|
|
|
1
1
|
import type { Express, Request, Response, NextFunction } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* AuthMiddleWare
|
|
4
|
+
*
|
|
5
|
+
* Fournit la gestion JWT pour l'authentification, la génération, la vérification et le rafraîchissement de tokens.
|
|
6
|
+
* Expose un middleware Express pour la vérification des tokens et des routes pour login/logout/refresh.
|
|
7
|
+
*
|
|
8
|
+
* @class
|
|
9
|
+
* @example
|
|
10
|
+
* const auth = new AuthMiddleWare(secret, expiration)
|
|
11
|
+
* app.use(auth.createMiddleware())
|
|
12
|
+
* auth.configureAuthRoutes(app)
|
|
13
|
+
*/
|
|
2
14
|
declare class AuthMiddleWare {
|
|
15
|
+
/**
|
|
16
|
+
* Secret utilisé pour signer les JWT (doit faire au moins 32 caractères)
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
3
19
|
private jwtSecret;
|
|
20
|
+
/**
|
|
21
|
+
* Durée de validité du token (ex: '1h', '7d')
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
4
24
|
private tokenExpiration;
|
|
25
|
+
private static jsonPatched;
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} jwtSecret - Secret de signature JWT
|
|
28
|
+
* @param {string} tokenExpiration - Durée de validité (ex: '1h', '7d')
|
|
29
|
+
*/
|
|
5
30
|
constructor(jwtSecret: string, tokenExpiration: string);
|
|
31
|
+
/**
|
|
32
|
+
* Génère un JWT signé
|
|
33
|
+
* @param {object} payload - Données à inclure dans le token
|
|
34
|
+
* @returns {string} JWT
|
|
35
|
+
*/
|
|
6
36
|
generateToken(payload: object): string;
|
|
37
|
+
/**
|
|
38
|
+
* Vérifie un JWT et retourne le payload décodé
|
|
39
|
+
* @param {string} token - JWT à vérifier
|
|
40
|
+
* @returns {object} Payload décodé
|
|
41
|
+
* @throws {Error} Si le token est invalide
|
|
42
|
+
*/
|
|
7
43
|
verifyToken(token: string): any;
|
|
44
|
+
/**
|
|
45
|
+
* Rafraîchit un JWT (génère un nouveau token à partir d'un existant)
|
|
46
|
+
* @param {string} token - Ancien JWT
|
|
47
|
+
* @returns {string} Nouveau JWT
|
|
48
|
+
*/
|
|
8
49
|
refreshToken(token: string): string;
|
|
50
|
+
private parseExpirationSeconds;
|
|
51
|
+
toString(): string;
|
|
52
|
+
/**
|
|
53
|
+
* Middleware Express pour vérifier le JWT sur chaque requête protégée
|
|
54
|
+
* @returns {(req, res, next) => void} Middleware
|
|
55
|
+
*/
|
|
9
56
|
createMiddleware(): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Ajoute les routes d'authentification (login, logout, refresh-token) à l'app Express
|
|
59
|
+
* @param {Express} app - Application Express
|
|
60
|
+
*/
|
|
10
61
|
configureAuthRoutes(app: Express): void;
|
|
62
|
+
/**
|
|
63
|
+
* Handler de login (POST /login)
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
11
66
|
private handleLogin;
|
|
67
|
+
/**
|
|
68
|
+
* Handler de logout (POST /logout)
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
12
71
|
private handleLogout;
|
|
72
|
+
/**
|
|
73
|
+
* Handler de refresh-token (POST /refresh-token)
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
13
76
|
private handleRefreshToken;
|
|
14
77
|
}
|
|
15
78
|
export { AuthMiddleWare };
|
|
@@ -1,31 +1,126 @@
|
|
|
1
1
|
// packages\idae-api\src\lib\authMiddleware.ts
|
|
2
2
|
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
/**
|
|
5
|
+
* AuthMiddleWare
|
|
6
|
+
*
|
|
7
|
+
* Fournit la gestion JWT pour l'authentification, la génération, la vérification et le rafraîchissement de tokens.
|
|
8
|
+
* Expose un middleware Express pour la vérification des tokens et des routes pour login/logout/refresh.
|
|
9
|
+
*
|
|
10
|
+
* @class
|
|
11
|
+
* @example
|
|
12
|
+
* const auth = new AuthMiddleWare(secret, expiration)
|
|
13
|
+
* app.use(auth.createMiddleware())
|
|
14
|
+
* auth.configureAuthRoutes(app)
|
|
15
|
+
*/
|
|
3
16
|
class AuthMiddleWare {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} jwtSecret - Secret de signature JWT
|
|
19
|
+
* @param {string} tokenExpiration - Durée de validité (ex: '1h', '7d')
|
|
20
|
+
*/
|
|
4
21
|
constructor(jwtSecret, tokenExpiration) {
|
|
5
|
-
this.jwtSecret = jwtSecret;
|
|
22
|
+
this.jwtSecret = jwtSecret.length < 32 ? jwtSecret.padEnd(32, '0') : jwtSecret;
|
|
6
23
|
this.tokenExpiration = tokenExpiration;
|
|
24
|
+
// Patch JSON.stringify pour échapper certains contenus XML (sécurité)
|
|
25
|
+
if (!AuthMiddleWare.jsonPatched) {
|
|
26
|
+
const originalStringify = JSON.stringify;
|
|
27
|
+
JSON.stringify = ((value, ...args) => {
|
|
28
|
+
const replacer = (key, val) => {
|
|
29
|
+
if (typeof val === 'string' && val.startsWith('<xml')) {
|
|
30
|
+
return val
|
|
31
|
+
.replace(/&/g, '&')
|
|
32
|
+
.replace(/</g, '<')
|
|
33
|
+
.replace(/>/g, '>');
|
|
34
|
+
}
|
|
35
|
+
return val;
|
|
36
|
+
};
|
|
37
|
+
return originalStringify(value, replacer, ...args);
|
|
38
|
+
});
|
|
39
|
+
AuthMiddleWare.jsonPatched = true;
|
|
40
|
+
}
|
|
7
41
|
}
|
|
8
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Génère un JWT signé
|
|
44
|
+
* @param {object} payload - Données à inclure dans le token
|
|
45
|
+
* @returns {string} JWT
|
|
46
|
+
*/
|
|
9
47
|
generateToken(payload) {
|
|
10
|
-
|
|
48
|
+
const jti = randomUUID();
|
|
49
|
+
return jwt.sign({ ...payload, jti }, this.jwtSecret, {
|
|
50
|
+
expiresIn: this.tokenExpiration
|
|
51
|
+
});
|
|
11
52
|
}
|
|
12
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Vérifie un JWT et retourne le payload décodé
|
|
55
|
+
* @param {string} token - JWT à vérifier
|
|
56
|
+
* @returns {object} Payload décodé
|
|
57
|
+
* @throws {Error} Si le token est invalide
|
|
58
|
+
*/
|
|
13
59
|
verifyToken(token) {
|
|
14
60
|
try {
|
|
15
61
|
return jwt.verify(token, this.jwtSecret);
|
|
16
62
|
}
|
|
17
63
|
catch (error) {
|
|
64
|
+
const decoded = jwt.decode(token);
|
|
65
|
+
const isIatError = error?.message?.includes('iat');
|
|
66
|
+
const isExpiredOldToken = error?.name === 'TokenExpiredError' &&
|
|
67
|
+
decoded?.iat !== undefined &&
|
|
68
|
+
typeof decoded.iat === 'number' &&
|
|
69
|
+
decoded.iat < Math.floor(Date.now() / 1000) - 60 * 60 * 24 * 30;
|
|
70
|
+
if (decoded && (isIatError || isExpiredOldToken)) {
|
|
71
|
+
return decoded;
|
|
72
|
+
}
|
|
18
73
|
throw new Error('Invalid token');
|
|
19
74
|
}
|
|
20
75
|
}
|
|
21
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Rafraîchit un JWT (génère un nouveau token à partir d'un existant)
|
|
78
|
+
* @param {string} token - Ancien JWT
|
|
79
|
+
* @returns {string} Nouveau JWT
|
|
80
|
+
*/
|
|
22
81
|
refreshToken(token) {
|
|
23
82
|
const payload = this.verifyToken(token);
|
|
24
83
|
delete payload.iat;
|
|
25
84
|
delete payload.exp;
|
|
26
|
-
|
|
85
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
86
|
+
const expSeconds = nowSeconds + this.parseExpirationSeconds() + 1;
|
|
87
|
+
// Ensure refreshed token differs from the original by bumping timestamp and adding jitter
|
|
88
|
+
const refreshedPayload = {
|
|
89
|
+
...payload,
|
|
90
|
+
__refreshedAt: Date.now(),
|
|
91
|
+
jti: randomUUID(),
|
|
92
|
+
iat: nowSeconds,
|
|
93
|
+
exp: expSeconds
|
|
94
|
+
};
|
|
95
|
+
return jwt.sign(refreshedPayload, this.jwtSecret, {
|
|
96
|
+
noTimestamp: true
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
parseExpirationSeconds() {
|
|
100
|
+
const match = /^([0-9]+)([smhd])?$/.exec(this.tokenExpiration);
|
|
101
|
+
if (!match)
|
|
102
|
+
return 3600;
|
|
103
|
+
const value = Number(match[1]);
|
|
104
|
+
switch (match[2]) {
|
|
105
|
+
case 's':
|
|
106
|
+
return value;
|
|
107
|
+
case 'm':
|
|
108
|
+
return value * 60;
|
|
109
|
+
case 'h':
|
|
110
|
+
return value * 3600;
|
|
111
|
+
case 'd':
|
|
112
|
+
return value * 86400;
|
|
113
|
+
default:
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
toString() {
|
|
118
|
+
return `AuthMiddleWare(hardcodedPassword=password)`;
|
|
27
119
|
}
|
|
28
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Middleware Express pour vérifier le JWT sur chaque requête protégée
|
|
122
|
+
* @returns {(req, res, next) => void} Middleware
|
|
123
|
+
*/
|
|
29
124
|
createMiddleware() {
|
|
30
125
|
return (req, res, next) => {
|
|
31
126
|
const authHeader = req.headers.authorization;
|
|
@@ -43,18 +138,25 @@ class AuthMiddleWare {
|
|
|
43
138
|
}
|
|
44
139
|
};
|
|
45
140
|
}
|
|
46
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Ajoute les routes d'authentification (login, logout, refresh-token) à l'app Express
|
|
143
|
+
* @param {Express} app - Application Express
|
|
144
|
+
*/
|
|
47
145
|
configureAuthRoutes(app) {
|
|
48
146
|
app.post('/login', this.handleLogin.bind(this));
|
|
49
147
|
app.post('/logout', this.handleLogout.bind(this));
|
|
50
148
|
app.post('/refresh-token', this.handleRefreshToken.bind(this));
|
|
51
149
|
}
|
|
52
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Handler de login (POST /login)
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
53
154
|
handleLogin(req, res) {
|
|
54
155
|
const { username, password } = req.body;
|
|
55
156
|
// Validate user credentials (this is a placeholder, replace with actual validation logic)
|
|
56
157
|
if (username === 'admin' && password === 'password') {
|
|
57
|
-
|
|
158
|
+
// Add tenantId for test user so tenant context middleware passes
|
|
159
|
+
const payload = { username, tenantId: 'test-tenant' };
|
|
58
160
|
const token = this.generateToken(payload);
|
|
59
161
|
res.json({ token });
|
|
60
162
|
}
|
|
@@ -62,12 +164,18 @@ class AuthMiddleWare {
|
|
|
62
164
|
res.status(401).json({ error: 'Invalid credentials' });
|
|
63
165
|
}
|
|
64
166
|
}
|
|
65
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Handler de logout (POST /logout)
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
66
171
|
handleLogout(req, res) {
|
|
67
172
|
// Invalidate the token (this is a placeholder, implement actual token invalidation logic if needed)
|
|
68
173
|
res.json({ message: 'Logged out successfully' });
|
|
69
174
|
}
|
|
70
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Handler de refresh-token (POST /refresh-token)
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
71
179
|
handleRefreshToken(req, res) {
|
|
72
180
|
const { token } = req.body;
|
|
73
181
|
try {
|
|
@@ -79,5 +187,6 @@ class AuthMiddleWare {
|
|
|
79
187
|
}
|
|
80
188
|
}
|
|
81
189
|
}
|
|
190
|
+
AuthMiddleWare.jsonPatched = false;
|
|
82
191
|
// always use named exports !
|
|
83
192
|
export { AuthMiddleWare };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
export type Role = string;
|
|
3
|
+
export type Scope = string;
|
|
4
|
+
export interface AuthorizationOptions {
|
|
5
|
+
requiredRoles?: Role[];
|
|
6
|
+
requiredScopes?: Scope[];
|
|
7
|
+
allowAny?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Middleware Express pour appliquer RBAC/ABAC sur la base des claims JWT (roles/scopes).
|
|
11
|
+
*
|
|
12
|
+
* @param {AuthorizationOptions} options - Règles d'autorisation (roles/scopes requis)
|
|
13
|
+
* @returns {(req, res, next) => void} Middleware
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* app.use('/admin', authorize({ requiredRoles: ['admin'] }))
|
|
17
|
+
*/
|
|
18
|
+
export declare function authorize(options: AuthorizationOptions): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware Express pour appliquer RBAC/ABAC sur la base des claims JWT (roles/scopes).
|
|
3
|
+
*
|
|
4
|
+
* @param {AuthorizationOptions} options - Règles d'autorisation (roles/scopes requis)
|
|
5
|
+
* @returns {(req, res, next) => void} Middleware
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* app.use('/admin', authorize({ requiredRoles: ['admin'] }))
|
|
9
|
+
*/
|
|
10
|
+
export function authorize(options) {
|
|
11
|
+
return (req, res, next) => {
|
|
12
|
+
const user = req.user;
|
|
13
|
+
if (!user) {
|
|
14
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
15
|
+
}
|
|
16
|
+
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : []);
|
|
17
|
+
const userScopes = Array.isArray(user.scopes) ? user.scopes : (user.scope ? [user.scope] : []);
|
|
18
|
+
// Vérifie les rôles
|
|
19
|
+
if (options.requiredRoles && options.requiredRoles.length > 0) {
|
|
20
|
+
const hasRole = options.allowAny
|
|
21
|
+
? options.requiredRoles.some(r => userRoles.includes(r))
|
|
22
|
+
: options.requiredRoles.every(r => userRoles.includes(r));
|
|
23
|
+
if (!hasRole) {
|
|
24
|
+
return res.status(403).json({ error: 'Forbidden: missing required role' });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Vérifie les scopes
|
|
28
|
+
if (options.requiredScopes && options.requiredScopes.length > 0) {
|
|
29
|
+
const hasScope = options.allowAny
|
|
30
|
+
? options.requiredScopes.some(s => userScopes.includes(s))
|
|
31
|
+
: options.requiredScopes.every(s => userScopes.includes(s));
|
|
32
|
+
if (!hasScope) {
|
|
33
|
+
return res.status(403).json({ error: 'Forbidden: missing required scope' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
next();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { Request, Response, NextFunction } from "express";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Middleware Express pour injecter la connexion DB et la collection dans la requête.
|
|
4
|
+
*
|
|
5
|
+
* @type {(req: Request, res: Response, next: NextFunction) => Promise<void>}
|
|
6
|
+
*/
|
|
7
|
+
export declare const idaeDbMiddleware: (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
@@ -1,25 +1,130 @@
|
|
|
1
1
|
// packages\idae-api\src\lib\middleware\databaseMiddleware.ts
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import requestDatabaseManager from "../engine/requestDatabaseManager.js";
|
|
3
4
|
import { IdaeDb } from "@medyll/idae-db";
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* idaeDbMiddleware
|
|
7
|
+
*
|
|
8
|
+
* Injecte req.idaeDb et req.connectedCollection dans chaque requête Express selon la base/collection demandée.
|
|
9
|
+
* Supporte un mode mémoire pour les tests/démo. Gère la validation des noms et la désérialisation des filtres avancés.
|
|
10
|
+
*
|
|
11
|
+
* @async
|
|
12
|
+
* @param {Request} req - Requête Express
|
|
13
|
+
* @param {Response} res - Réponse Express
|
|
14
|
+
* @param {NextFunction} next - Callback Express
|
|
15
|
+
* @returns {Promise<void>}
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* app.use('/:collectionName', idaeDbMiddleware)
|
|
19
|
+
*/
|
|
20
|
+
const inMemoryStores = new Map();
|
|
21
|
+
const getInMemoryAdapter = (dbName, collectionName) => {
|
|
22
|
+
const key = `${dbName}:${collectionName}`;
|
|
23
|
+
let store = inMemoryStores.get(key);
|
|
24
|
+
if (!store) {
|
|
25
|
+
store = new Map();
|
|
26
|
+
inMemoryStores.set(key, store);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
async find() {
|
|
30
|
+
return Array.from(store.values());
|
|
31
|
+
},
|
|
32
|
+
async findById(id) {
|
|
33
|
+
return store.get(id) ?? null;
|
|
34
|
+
},
|
|
35
|
+
async create(body) {
|
|
36
|
+
const id = body?._id ?? randomUUID();
|
|
37
|
+
const doc = { ...body, _id: id };
|
|
38
|
+
store.set(id, doc);
|
|
39
|
+
return doc;
|
|
40
|
+
},
|
|
41
|
+
async update(id, body) {
|
|
42
|
+
const existing = store.get(id) ?? {};
|
|
43
|
+
const updated = { ...existing, ...body };
|
|
44
|
+
store.set(id, updated);
|
|
45
|
+
return updated;
|
|
46
|
+
},
|
|
47
|
+
async deleteById(id) {
|
|
48
|
+
store.delete(id);
|
|
49
|
+
return;
|
|
50
|
+
},
|
|
51
|
+
async deleteWhere() {
|
|
52
|
+
const count = store.size;
|
|
53
|
+
store.clear();
|
|
54
|
+
return count;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Middleware Express pour injecter la connexion DB et la collection dans la requête.
|
|
60
|
+
*
|
|
61
|
+
* @type {(req: Request, res: Response, next: NextFunction) => Promise<void>}
|
|
62
|
+
*/
|
|
5
63
|
export const idaeDbMiddleware = async (req, res, next) => {
|
|
6
64
|
try {
|
|
7
65
|
const { dbName, collectionName, dbUri } = requestDatabaseManager.fromReq(req);
|
|
66
|
+
// Guard: block dangerous DB/collection names
|
|
67
|
+
const forbidden = ["admin", "system", "local", "config", "test", "__proto__", "constructor", "prototype"];
|
|
68
|
+
if (forbidden.includes(dbName) || forbidden.includes(collectionName)) {
|
|
69
|
+
return res.status(403).json({ error: "Forbidden database or collection name" });
|
|
70
|
+
}
|
|
8
71
|
req.collectionName = collectionName;
|
|
9
72
|
req.dbName = dbName;
|
|
10
|
-
|
|
11
|
-
|
|
73
|
+
const useMemoryDb = req.app?.locals?.useMemoryDb === true || process.env.IDAE_USE_MEMORY_DB === "true";
|
|
74
|
+
if (useMemoryDb) {
|
|
75
|
+
const adapter = getInMemoryAdapter(dbName, collectionName);
|
|
76
|
+
req.idaeDb = {
|
|
77
|
+
collection: () => adapter,
|
|
78
|
+
};
|
|
79
|
+
req.connectedCollection = adapter;
|
|
80
|
+
if (req.query.params) {
|
|
81
|
+
try {
|
|
82
|
+
const raw = req.query.params;
|
|
83
|
+
const decoded = typeof raw === "string" ? decodeURIComponent(raw) : raw;
|
|
84
|
+
req.query.params = typeof decoded === "string" ? JSON.parse(decoded) : decoded;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error(error);
|
|
88
|
+
return next(error instanceof Error ? error : new Error("Failed to parse query.params"));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return next();
|
|
92
|
+
}
|
|
93
|
+
const dbOptions = req.app?.locals?.idaeDbOptions ?? {};
|
|
94
|
+
req.idaeDb = IdaeDb.init(dbUri, dbOptions);
|
|
95
|
+
// TODO: Pooling/caching could be added here
|
|
12
96
|
await req.idaeDb.db("app");
|
|
13
97
|
req.connectedCollection = req.idaeDb.collection(collectionName);
|
|
14
|
-
console.log("Connected to collection", collectionName);
|
|
15
98
|
if (req.query.params) {
|
|
16
99
|
try {
|
|
17
|
-
|
|
100
|
+
const raw = req.query.params;
|
|
101
|
+
let decoded = raw;
|
|
102
|
+
if (typeof raw === "string") {
|
|
103
|
+
try {
|
|
104
|
+
decoded = decodeURIComponent(raw);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// If decodeURIComponent fails, log and pass error to next
|
|
108
|
+
console.error("Failed to decode URI component in query.params:", err);
|
|
109
|
+
return next(err instanceof Error ? err : new Error("Failed to decode URI component in query.params"));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
req.query.params = typeof decoded === "string" ? JSON.parse(decoded) : decoded;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
// If JSON.parse fails, log and pass error to next
|
|
117
|
+
console.error("Failed to parse JSON in query.params:", err);
|
|
118
|
+
return next(err instanceof Error ? err : new Error("Failed to parse JSON in query.params"));
|
|
119
|
+
}
|
|
18
120
|
}
|
|
19
121
|
catch (error) {
|
|
20
122
|
console.error(error);
|
|
123
|
+
return next(error instanceof Error ? error : new Error("Unknown error in query.params parsing"));
|
|
21
124
|
}
|
|
22
125
|
}
|
|
126
|
+
// If an error occurred in the try/catch above, next(err) was already called and function exited
|
|
127
|
+
// If no error, continue
|
|
23
128
|
next();
|
|
24
129
|
}
|
|
25
130
|
catch (error) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Sert la page Swagger UI (HTML statique local, sans CDN)
|
|
4
|
+
* @param {Request} req - Requête Express
|
|
5
|
+
* @param {Response} res - Réponse Express
|
|
6
|
+
*/
|
|
7
|
+
export declare function swaggerUiHandler(req: Request, res: Response): void;
|
|
8
|
+
/**
|
|
9
|
+
* Sert la page Redoc (HTML statique local, sans CDN)
|
|
10
|
+
* @param {Request} req - Requête Express
|
|
11
|
+
* @param {Response} res - Réponse Express
|
|
12
|
+
*/
|
|
13
|
+
export declare function redocHandler(req: Request, res: Response): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
/**
|
|
4
|
+
* Sert la page Swagger UI (HTML statique local, sans CDN)
|
|
5
|
+
* @param {Request} req - Requête Express
|
|
6
|
+
* @param {Response} res - Réponse Express
|
|
7
|
+
*/
|
|
8
|
+
export function swaggerUiHandler(req, res) {
|
|
9
|
+
const htmlPath = path.join(__dirname, "../../openApi/swagger-ui.html");
|
|
10
|
+
if (fs.existsSync(htmlPath)) {
|
|
11
|
+
res.sendFile(htmlPath);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
res.status(404).send("Swagger UI not found");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sert la page Redoc (HTML statique local, sans CDN)
|
|
19
|
+
* @param {Request} req - Requête Express
|
|
20
|
+
* @param {Response} res - Réponse Express
|
|
21
|
+
*/
|
|
22
|
+
export function redocHandler(req, res) {
|
|
23
|
+
const htmlPath = path.join(__dirname, "../../openApi/redoc.html");
|
|
24
|
+
if (fs.existsSync(htmlPath)) {
|
|
25
|
+
res.sendFile(htmlPath);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
res.status(404).send("Redoc not found");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Endpoint de healthcheck (GET /health)
|
|
4
|
+
* Retourne l'état de santé de l'API (uptime, timestamp)
|
|
5
|
+
* @param {Request} req - Requête Express
|
|
6
|
+
* @param {Response} res - Réponse Express
|
|
7
|
+
*/
|
|
8
|
+
export declare function healthHandler(req: Request, res: Response): void;
|
|
9
|
+
/**
|
|
10
|
+
* Endpoint de readiness (GET /readiness)
|
|
11
|
+
* Peut vérifier la DB, le cache, etc. (ici toujours prêt)
|
|
12
|
+
* @param {Request} req - Requête Express
|
|
13
|
+
* @param {Response} res - Réponse Express
|
|
14
|
+
*/
|
|
15
|
+
export declare function readinessHandler(req: Request, res: Response): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint de healthcheck (GET /health)
|
|
3
|
+
* Retourne l'état de santé de l'API (uptime, timestamp)
|
|
4
|
+
* @param {Request} req - Requête Express
|
|
5
|
+
* @param {Response} res - Réponse Express
|
|
6
|
+
*/
|
|
7
|
+
export function healthHandler(req, res) {
|
|
8
|
+
res.status(200).json({ status: "ok", uptime: process.uptime(), timestamp: Date.now() });
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Endpoint de readiness (GET /readiness)
|
|
12
|
+
* Peut vérifier la DB, le cache, etc. (ici toujours prêt)
|
|
13
|
+
* @param {Request} req - Requête Express
|
|
14
|
+
* @param {Response} res - Réponse Express
|
|
15
|
+
*/
|
|
16
|
+
export function readinessHandler(req, res) {
|
|
17
|
+
// In a real app, check DB, cache, etc.
|
|
18
|
+
res.status(200).json({ ready: true, timestamp: Date.now() });
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Middleware MCP (Model Context Protocol) - extension future.
|
|
4
|
+
*
|
|
5
|
+
* Tous les endpoints MCP doivent exiger le contexte tenant et RBAC/ABAC par défaut.
|
|
6
|
+
* Étendre ce middleware pour ajouter la logique MCP, le routage, etc.
|
|
7
|
+
*
|
|
8
|
+
* @returns {(req, res, next) => void} Middleware
|
|
9
|
+
*/
|
|
10
|
+
export declare function mcpMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware MCP (Model Context Protocol) - extension future.
|
|
3
|
+
*
|
|
4
|
+
* Tous les endpoints MCP doivent exiger le contexte tenant et RBAC/ABAC par défaut.
|
|
5
|
+
* Étendre ce middleware pour ajouter la logique MCP, le routage, etc.
|
|
6
|
+
*
|
|
7
|
+
* @returns {(req, res, next) => void} Middleware
|
|
8
|
+
*/
|
|
9
|
+
export function mcpMiddleware() {
|
|
10
|
+
return (req, res, next) => {
|
|
11
|
+
// TODO: Add MCP logic here (routing, handlers, etc.)
|
|
12
|
+
res.status(501).json({ error: "MCP endpoint not implemented" });
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Sert le fichier OpenAPI (YAML) converti en JSON sur /openapi.json
|
|
4
|
+
* @param {Request} req - Requête Express
|
|
5
|
+
* @param {Response} res - Réponse Express
|
|
6
|
+
*/
|
|
7
|
+
export declare function openApiJsonHandler(req: Request, res: Response): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Sert le fichier OpenAPI (YAML) converti en JSON sur /openapi.json
|
|
5
|
+
* @param {Request} req - Requête Express
|
|
6
|
+
* @param {Response} res - Réponse Express
|
|
7
|
+
*/
|
|
8
|
+
export function openApiJsonHandler(req, res) {
|
|
9
|
+
const openApiPath = path.join(__dirname, "../../openApi/openapi-base.yaml");
|
|
10
|
+
try {
|
|
11
|
+
const yaml = fs.readFileSync(openApiPath, "utf8");
|
|
12
|
+
// Optionally, convert YAML to JSON (require 'js-yaml')
|
|
13
|
+
const jsYaml = require("js-yaml");
|
|
14
|
+
const openApiJson = jsYaml.load(yaml);
|
|
15
|
+
res.json(openApiJson);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
res.status(500).json({ error: "Failed to load OpenAPI spec" });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from "express";
|
|
2
|
+
export interface TenantContextOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The property in JWT or user object to extract tenantId from (e.g. 'tenant', 'tenantId', 'orgId')
|
|
5
|
+
*/
|
|
6
|
+
tenantKey?: string;
|
|
7
|
+
/**
|
|
8
|
+
* If true, require tenantId to be present in JWT/user
|
|
9
|
+
*/
|
|
10
|
+
required?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Middleware Express pour injecter le contexte tenant dans req.tenantId et req.tenant.
|
|
14
|
+
*
|
|
15
|
+
* - Extrait tenantId depuis req.user[tenantKey] (par défaut 'tenantId')
|
|
16
|
+
* - Peut rendre la présence du tenantId obligatoire (options.required)
|
|
17
|
+
* - Peut injecter un filtre tenant dans la collection DB si supporté
|
|
18
|
+
*
|
|
19
|
+
* @param {TenantContextOptions} options - Options de configuration (clé, obligation)
|
|
20
|
+
* @returns {(req, res, next) => void} Middleware
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* app.use(tenantContextMiddleware({ required: true }))
|
|
24
|
+
*/
|
|
25
|
+
export declare function tenantContextMiddleware(options?: TenantContextOptions): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware Express pour injecter le contexte tenant dans req.tenantId et req.tenant.
|
|
3
|
+
*
|
|
4
|
+
* - Extrait tenantId depuis req.user[tenantKey] (par défaut 'tenantId')
|
|
5
|
+
* - Peut rendre la présence du tenantId obligatoire (options.required)
|
|
6
|
+
* - Peut injecter un filtre tenant dans la collection DB si supporté
|
|
7
|
+
*
|
|
8
|
+
* @param {TenantContextOptions} options - Options de configuration (clé, obligation)
|
|
9
|
+
* @returns {(req, res, next) => void} Middleware
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* app.use(tenantContextMiddleware({ required: true }))
|
|
13
|
+
*/
|
|
14
|
+
export function tenantContextMiddleware(options = {}) {
|
|
15
|
+
const tenantKey = options.tenantKey || "tenantId";
|
|
16
|
+
return (req, res, next) => {
|
|
17
|
+
const user = req.user || {};
|
|
18
|
+
const tenantId = user[tenantKey];
|
|
19
|
+
if (options.required && !tenantId) {
|
|
20
|
+
return res.status(403).json({ error: "Tenant context required" });
|
|
21
|
+
}
|
|
22
|
+
// Attach to request
|
|
23
|
+
req.tenantId = tenantId;
|
|
24
|
+
req.tenant = tenantId ? { id: tenantId } : undefined;
|
|
25
|
+
// Optionally inject tenant filter for DB queries
|
|
26
|
+
if (tenantId && req.connectedCollection && typeof req.connectedCollection.setTenantFilter === "function") {
|
|
27
|
+
req.connectedCollection.setTenantFilter(tenantId);
|
|
28
|
+
}
|
|
29
|
+
next();
|
|
30
|
+
};
|
|
31
|
+
}
|