impulse-api 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -3
- package/readme.md +88 -0
- package/sample-routes/custom-jwt-example.js +83 -0
- package/src/auth.js +7 -0
- package/src/server.js +14 -1
- package/test/custom-jwt-validation.js +298 -0
package/package.json
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
"name": "impulse-api",
|
|
5
5
|
"description": "A efficient and powerful API with Express under the hood.",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"pre-commit": [
|
|
8
|
+
"test"
|
|
9
|
+
],
|
|
7
10
|
"scripts": {
|
|
8
11
|
"lint": "eslint --ignore-path .gitignore .",
|
|
9
12
|
"test": "nyc --reporter=lcov --reporter=text-summary mocha",
|
|
10
13
|
"test-server": "node ./test/integration/test-server.js"
|
|
11
14
|
},
|
|
12
|
-
"version": "3.0.
|
|
15
|
+
"version": "3.0.2",
|
|
13
16
|
"engines": {
|
|
14
17
|
"node": ">=22"
|
|
15
18
|
},
|
|
@@ -19,9 +22,9 @@
|
|
|
19
22
|
"express": "4.21.2",
|
|
20
23
|
"express-fileupload": "1.5.1",
|
|
21
24
|
"http-errors": "2.0.0",
|
|
22
|
-
"jsonwebtoken": "
|
|
25
|
+
"jsonwebtoken": "8.5.1",
|
|
23
26
|
"lodash": "4.17.21",
|
|
24
|
-
"morgan": "
|
|
27
|
+
"morgan": "1.10.0",
|
|
25
28
|
"multer": "1.4.5-lts.2",
|
|
26
29
|
"path-to-regexp": "0.1.12"
|
|
27
30
|
},
|
|
@@ -29,6 +32,7 @@
|
|
|
29
32
|
"eslint": "^9.23.0",
|
|
30
33
|
"eslint-plugin-import": "^2.31.0",
|
|
31
34
|
"nyc": "^17.1.0",
|
|
35
|
+
"pre-commit": "^1.2.2",
|
|
32
36
|
"sinon": "4.4.6"
|
|
33
37
|
},
|
|
34
38
|
"keywords": [
|
package/readme.md
CHANGED
|
@@ -107,6 +107,94 @@ const { Auth } = require('impulse-api');
|
|
|
107
107
|
TokenExpiredError: [Function: TokenExpiredError] }
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
#### Custom JWT Validation
|
|
111
|
+
|
|
112
|
+
You can override JWT validation to use custom authentication providers (like Google OAuth, Auth0, etc.) instead of the default JWT validation. This is typically configured once at the server level.
|
|
113
|
+
|
|
114
|
+
##### Server-Level Custom Validation (Recommended)
|
|
115
|
+
```javascript
|
|
116
|
+
const config = {
|
|
117
|
+
name: 'my-api',
|
|
118
|
+
port: 3000,
|
|
119
|
+
secretKey: 'fallback-secret',
|
|
120
|
+
routeDir: './routes',
|
|
121
|
+
tokenValidator: async (token, services) => {
|
|
122
|
+
// Global token validation for all routes with tokenAuth: true
|
|
123
|
+
const userInfo = await services.googleOAuth.verifyToken(token);
|
|
124
|
+
return {
|
|
125
|
+
userId: userInfo.googleId,
|
|
126
|
+
email: userInfo.email,
|
|
127
|
+
name: userInfo.name,
|
|
128
|
+
picture: userInfo.picture
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
services: {
|
|
132
|
+
googleOAuth: {
|
|
133
|
+
verifyToken: async (token) => {
|
|
134
|
+
// Your Google OAuth validation logic
|
|
135
|
+
// This would typically make an API call to Google
|
|
136
|
+
return {
|
|
137
|
+
googleId: 'google-123',
|
|
138
|
+
email: 'user@example.com',
|
|
139
|
+
name: 'John Doe',
|
|
140
|
+
picture: 'https://example.com/photo.jpg'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const api = new Impulse(config);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
##### Per-Route Override (Optional)
|
|
151
|
+
If you need different validation for specific routes, you can override the global validator:
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
exports.specialRoute = {
|
|
155
|
+
name: 'specialRoute',
|
|
156
|
+
method: 'post',
|
|
157
|
+
endpoint: '/api/special',
|
|
158
|
+
version: 'v1',
|
|
159
|
+
tokenAuth: true,
|
|
160
|
+
validateToken: async (token, services) => {
|
|
161
|
+
// Override global validation for this specific route
|
|
162
|
+
const userInfo = await services.specialAuth.verifyToken(token);
|
|
163
|
+
return {
|
|
164
|
+
userId: userInfo.specialId,
|
|
165
|
+
role: userInfo.role,
|
|
166
|
+
permissions: userInfo.permissions
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
run: (services, inputs, next) => {
|
|
170
|
+
const user = inputs.decoded; // From route-specific validation
|
|
171
|
+
next(200, { message: 'Success', user });
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
##### Validation Flow
|
|
177
|
+
1. **Global token validator** (`config.tokenValidator`) - primary validation
|
|
178
|
+
2. **Route-specific validator** (`route.validateToken`) - optional override
|
|
179
|
+
3. **Default JWT validation** - fallback when no token validator is provided
|
|
180
|
+
|
|
181
|
+
##### Backward Compatibility
|
|
182
|
+
Existing routes without custom validators continue to work with default JWT validation. No breaking changes to the existing API.
|
|
183
|
+
|
|
184
|
+
##### Error Handling
|
|
185
|
+
Custom validators should throw errors for invalid tokens. The framework will catch these and return appropriate HTTP 401 responses.
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
validateToken: async (token, services) => {
|
|
189
|
+
try {
|
|
190
|
+
const userInfo = await services.oauth.verifyToken(token);
|
|
191
|
+
return userInfo;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new Error('Invalid authentication token');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
110
198
|
#### Application Auth
|
|
111
199
|
For `admin` type routes, Impulse-Api also provides `applicationAuth` which secures any routes behind basic key authorization provided by `appKey` in the server configuration. The `applicationAuth` can be verified and passed in the header, parameters, query, or body as `key`.
|
|
112
200
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Example route using server-level custom JWT validation for Google OAuth
|
|
2
|
+
// The tokenValidator is configured at server initialization
|
|
3
|
+
exports.scanDocument = {
|
|
4
|
+
name: 'scanDocument',
|
|
5
|
+
description: 'Scan a document with Google OAuth authentication',
|
|
6
|
+
method: 'post',
|
|
7
|
+
endpoint: '/api/scan',
|
|
8
|
+
version: 'v1',
|
|
9
|
+
tokenAuth: true,
|
|
10
|
+
// No custom validator needed - uses server-level tokenValidator
|
|
11
|
+
inputs: {
|
|
12
|
+
documentId: {
|
|
13
|
+
required: true,
|
|
14
|
+
validate: (value) => typeof value === 'string' && value.length > 0
|
|
15
|
+
},
|
|
16
|
+
scanType: {
|
|
17
|
+
required: false,
|
|
18
|
+
validate: (value) => ['text', 'image', 'pdf'].includes(value)
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
run: (services, inputs, next) => {
|
|
22
|
+
const user = inputs.decoded; // Contains Google OAuth user data from server-level tokenValidator
|
|
23
|
+
const { documentId, scanType } = inputs;
|
|
24
|
+
|
|
25
|
+
// Process the document scan with authenticated user
|
|
26
|
+
const result = {
|
|
27
|
+
success: true,
|
|
28
|
+
documentId,
|
|
29
|
+
scanType: scanType || 'text',
|
|
30
|
+
user: {
|
|
31
|
+
id: user.userId,
|
|
32
|
+
email: user.email,
|
|
33
|
+
name: user.name,
|
|
34
|
+
picture: user.picture
|
|
35
|
+
},
|
|
36
|
+
timestamp: new Date().toISOString()
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
next(200, result);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Example route using global custom validator
|
|
44
|
+
exports.getUserProfile = {
|
|
45
|
+
name: 'getUserProfile',
|
|
46
|
+
description: 'Get user profile with custom authentication',
|
|
47
|
+
method: 'get',
|
|
48
|
+
endpoint: '/api/profile',
|
|
49
|
+
version: 'v1',
|
|
50
|
+
tokenAuth: true,
|
|
51
|
+
// This route will use the global tokenValidator from config
|
|
52
|
+
run: (services, inputs, next) => {
|
|
53
|
+
const user = inputs.decoded; // From global tokenValidator
|
|
54
|
+
|
|
55
|
+
const profile = {
|
|
56
|
+
userId: user.userId,
|
|
57
|
+
email: user.email,
|
|
58
|
+
lastLogin: new Date().toISOString(),
|
|
59
|
+
preferences: user.preferences || {}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
next(200, profile);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Example route with default JWT validation (backward compatibility)
|
|
67
|
+
exports.legacyRoute = {
|
|
68
|
+
name: 'legacyRoute',
|
|
69
|
+
description: 'Legacy route using default JWT validation',
|
|
70
|
+
method: 'get',
|
|
71
|
+
endpoint: '/api/legacy',
|
|
72
|
+
version: 'v1',
|
|
73
|
+
tokenAuth: true,
|
|
74
|
+
// No custom validator - uses default JWT validation
|
|
75
|
+
run: (services, inputs, next) => {
|
|
76
|
+
const user = inputs.decoded; // From default JWT validation
|
|
77
|
+
|
|
78
|
+
next(200, {
|
|
79
|
+
message: 'Legacy route works with default JWT',
|
|
80
|
+
user: user
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
package/src/auth.js
CHANGED
|
@@ -5,6 +5,13 @@ const auth = {
|
|
|
5
5
|
},
|
|
6
6
|
verifyToken(token, secretKey) {
|
|
7
7
|
return jwt.verify(token, secretKey);
|
|
8
|
+
},
|
|
9
|
+
// Helper function to create custom validators
|
|
10
|
+
createCustomValidator(validatorFn) {
|
|
11
|
+
if (typeof validatorFn !== 'function') {
|
|
12
|
+
throw new Error('Custom validator must be a function');
|
|
13
|
+
}
|
|
14
|
+
return validatorFn;
|
|
8
15
|
}
|
|
9
16
|
};
|
|
10
17
|
|
package/src/server.js
CHANGED
|
@@ -36,6 +36,7 @@ class Server {
|
|
|
36
36
|
this.heartbeat = config.heartbeat || null;
|
|
37
37
|
this.container = new Container(config.services);
|
|
38
38
|
this.secretKey = config.secretKey;
|
|
39
|
+
this.tokenValidator = config.tokenValidator || null;
|
|
39
40
|
this.errors = config.errors || Errors;
|
|
40
41
|
process.env.NODE_ENV = this.env || 'dev';
|
|
41
42
|
this.configureHttpServer();
|
|
@@ -214,7 +215,19 @@ class Server {
|
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
try {
|
|
217
|
-
|
|
218
|
+
// Use global token validator if provided, otherwise fall back to default JWT validation
|
|
219
|
+
if (this.tokenValidator && typeof this.tokenValidator === 'function') {
|
|
220
|
+
decoded = await this.tokenValidator(token, this.container);
|
|
221
|
+
} else {
|
|
222
|
+
// Default JWT validation
|
|
223
|
+
decoded = await Auth.verifyToken(token, this.secretKey);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Allow route-specific validation to override the global validator
|
|
227
|
+
if (route.validateToken && typeof route.validateToken === 'function') {
|
|
228
|
+
decoded = await route.validateToken(token, this.container);
|
|
229
|
+
}
|
|
230
|
+
|
|
218
231
|
if (decoded.username) {
|
|
219
232
|
username = decoded.username;
|
|
220
233
|
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const Api = require('../src/api');
|
|
3
|
+
const Auth = require('../src/auth');
|
|
4
|
+
|
|
5
|
+
describe('Custom JWT Validation', () => {
|
|
6
|
+
let testServer;
|
|
7
|
+
let testRouteDir;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Create a temporary route directory for testing
|
|
11
|
+
testRouteDir = '/tmp/test-routes';
|
|
12
|
+
require('fs').mkdirSync(testRouteDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
// Clean up test route directory
|
|
17
|
+
if (require('fs').existsSync(testRouteDir)) {
|
|
18
|
+
require('fs').rmSync(testRouteDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('Server-Level Custom Validator', () => {
|
|
23
|
+
it('should use server-level custom validator when provided', async () => {
|
|
24
|
+
const customValidator = async (token, services) => {
|
|
25
|
+
if (token === 'valid-custom-token') {
|
|
26
|
+
return {
|
|
27
|
+
userId: 'test-user-123',
|
|
28
|
+
email: 'test@example.com',
|
|
29
|
+
customField: 'custom-value'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
throw new Error('Invalid token');
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const config = {
|
|
36
|
+
name: 'test-api',
|
|
37
|
+
routeDir: testRouteDir,
|
|
38
|
+
secretKey: 'test-secret',
|
|
39
|
+
tokenValidator: customValidator,
|
|
40
|
+
services: {
|
|
41
|
+
testService: { name: 'test' }
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Create a test route file
|
|
46
|
+
const testRoute = `
|
|
47
|
+
exports.testRoute = {
|
|
48
|
+
name: 'testRoute',
|
|
49
|
+
method: 'get',
|
|
50
|
+
endpoint: '/test',
|
|
51
|
+
version: 'v1',
|
|
52
|
+
tokenAuth: true,
|
|
53
|
+
run: (services, inputs, next) => {
|
|
54
|
+
next(200, {
|
|
55
|
+
message: 'Success',
|
|
56
|
+
user: inputs.decoded
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
`;
|
|
61
|
+
require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
|
|
62
|
+
|
|
63
|
+
const api = new Api(config);
|
|
64
|
+
await api.init();
|
|
65
|
+
|
|
66
|
+
// Test would require actual HTTP request, but we can test the validator function directly
|
|
67
|
+
const result = await customValidator('valid-custom-token', {});
|
|
68
|
+
assert.deepStrictEqual(result, {
|
|
69
|
+
userId: 'test-user-123',
|
|
70
|
+
email: 'test@example.com',
|
|
71
|
+
customField: 'custom-value'
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should fall back to default JWT validation when no custom validator', async () => {
|
|
76
|
+
const config = {
|
|
77
|
+
name: 'test-api',
|
|
78
|
+
routeDir: testRouteDir,
|
|
79
|
+
secretKey: 'test-secret',
|
|
80
|
+
services: {}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Create a test route file
|
|
84
|
+
const testRoute = `
|
|
85
|
+
exports.testRoute = {
|
|
86
|
+
name: 'testRoute',
|
|
87
|
+
method: 'get',
|
|
88
|
+
endpoint: '/test',
|
|
89
|
+
version: 'v1',
|
|
90
|
+
tokenAuth: true,
|
|
91
|
+
run: (services, inputs, next) => {
|
|
92
|
+
next(200, { message: 'Success' });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
`;
|
|
96
|
+
require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
|
|
97
|
+
|
|
98
|
+
const api = new Api(config);
|
|
99
|
+
await api.init();
|
|
100
|
+
|
|
101
|
+
// Test default JWT validation
|
|
102
|
+
const token = Auth.generateToken({ userId: 'test-user' }, { secretKey: 'test-secret' });
|
|
103
|
+
const decoded = Auth.verifyToken(token, 'test-secret');
|
|
104
|
+
assert.strictEqual(decoded.userId, 'test-user');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('Route-Specific Override', () => {
|
|
109
|
+
it('should allow route-specific validator to override server-level validator', async () => {
|
|
110
|
+
const serverValidator = async (token, services) => {
|
|
111
|
+
return { userId: 'server-user', source: 'server' };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const routeValidator = async (token, services) => {
|
|
115
|
+
if (token === 'route-specific-token') {
|
|
116
|
+
return {
|
|
117
|
+
userId: 'route-user-456',
|
|
118
|
+
email: 'route@example.com',
|
|
119
|
+
source: 'route'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
throw new Error('Invalid route token');
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const config = {
|
|
126
|
+
name: 'test-api',
|
|
127
|
+
routeDir: testRouteDir,
|
|
128
|
+
secretKey: 'test-secret',
|
|
129
|
+
tokenValidator: serverValidator,
|
|
130
|
+
services: {}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Create a test route file with custom validator override
|
|
134
|
+
const testRoute = `
|
|
135
|
+
exports.testRoute = {
|
|
136
|
+
name: 'testRoute',
|
|
137
|
+
method: 'get',
|
|
138
|
+
endpoint: '/test',
|
|
139
|
+
version: 'v1',
|
|
140
|
+
tokenAuth: true,
|
|
141
|
+
validateToken: async (token, services) => {
|
|
142
|
+
if (token === 'route-specific-token') {
|
|
143
|
+
return {
|
|
144
|
+
userId: 'route-user-456',
|
|
145
|
+
email: 'route@example.com',
|
|
146
|
+
source: 'route'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
throw new Error('Invalid route token');
|
|
150
|
+
},
|
|
151
|
+
run: (services, inputs, next) => {
|
|
152
|
+
next(200, {
|
|
153
|
+
message: 'Success',
|
|
154
|
+
user: inputs.decoded
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
`;
|
|
159
|
+
require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
|
|
160
|
+
|
|
161
|
+
const api = new Api(config);
|
|
162
|
+
await api.init();
|
|
163
|
+
|
|
164
|
+
// Test the route-specific validator
|
|
165
|
+
const result = await routeValidator('route-specific-token', {});
|
|
166
|
+
assert.deepStrictEqual(result, {
|
|
167
|
+
userId: 'route-user-456',
|
|
168
|
+
email: 'route@example.com',
|
|
169
|
+
source: 'route'
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should use server-level validator when no route-specific validator', async () => {
|
|
174
|
+
const serverValidator = async (token, services) => {
|
|
175
|
+
return { userId: 'server-user', source: 'server' };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const config = {
|
|
179
|
+
name: 'test-api',
|
|
180
|
+
routeDir: testRouteDir,
|
|
181
|
+
secretKey: 'test-secret',
|
|
182
|
+
tokenValidator: serverValidator,
|
|
183
|
+
services: {}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Create a test route file without custom validator
|
|
187
|
+
const testRoute = `
|
|
188
|
+
exports.testRoute = {
|
|
189
|
+
name: 'testRoute',
|
|
190
|
+
method: 'get',
|
|
191
|
+
endpoint: '/test',
|
|
192
|
+
version: 'v1',
|
|
193
|
+
tokenAuth: true,
|
|
194
|
+
run: (services, inputs, next) => {
|
|
195
|
+
next(200, {
|
|
196
|
+
message: 'Success',
|
|
197
|
+
user: inputs.decoded
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
`;
|
|
202
|
+
require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
|
|
203
|
+
|
|
204
|
+
const api = new Api(config);
|
|
205
|
+
await api.init();
|
|
206
|
+
|
|
207
|
+
// Test that server validator is used
|
|
208
|
+
const result = await serverValidator('any-token', {});
|
|
209
|
+
assert.strictEqual(result.source, 'server');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('Error Handling', () => {
|
|
214
|
+
it('should handle custom validator errors gracefully', async () => {
|
|
215
|
+
const failingValidator = async (token, services) => {
|
|
216
|
+
throw new Error('Custom validation failed');
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const config = {
|
|
220
|
+
name: 'test-api',
|
|
221
|
+
routeDir: testRouteDir,
|
|
222
|
+
secretKey: 'test-secret',
|
|
223
|
+
tokenValidator: failingValidator,
|
|
224
|
+
services: {}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Create a test route file
|
|
228
|
+
const testRoute = `
|
|
229
|
+
exports.testRoute = {
|
|
230
|
+
name: 'testRoute',
|
|
231
|
+
method: 'get',
|
|
232
|
+
endpoint: '/test',
|
|
233
|
+
version: 'v1',
|
|
234
|
+
tokenAuth: true,
|
|
235
|
+
run: (services, inputs, next) => {
|
|
236
|
+
next(200, { message: 'Success' });
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
`;
|
|
240
|
+
require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
|
|
241
|
+
|
|
242
|
+
const api = new Api(config);
|
|
243
|
+
await api.init();
|
|
244
|
+
|
|
245
|
+
// Test that validator throws expected error
|
|
246
|
+
try {
|
|
247
|
+
await failingValidator('invalid-token', {});
|
|
248
|
+
assert.fail('Should have thrown an error');
|
|
249
|
+
} catch (error) {
|
|
250
|
+
assert.strictEqual(error.message, 'Custom validation failed');
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should validate custom validator function type', () => {
|
|
255
|
+
assert.throws(() => {
|
|
256
|
+
Auth.createCustomValidator('not-a-function');
|
|
257
|
+
}, /Custom validator must be a function/);
|
|
258
|
+
|
|
259
|
+
assert.doesNotThrow(() => {
|
|
260
|
+
Auth.createCustomValidator(() => {});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('Backward Compatibility', () => {
|
|
266
|
+
it('should work with existing routes without custom validation', async () => {
|
|
267
|
+
const config = {
|
|
268
|
+
name: 'test-api',
|
|
269
|
+
routeDir: testRouteDir,
|
|
270
|
+
secretKey: 'test-secret',
|
|
271
|
+
services: {}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Create a test route file without custom validation
|
|
275
|
+
const testRoute = `
|
|
276
|
+
exports.testRoute = {
|
|
277
|
+
name: 'testRoute',
|
|
278
|
+
method: 'get',
|
|
279
|
+
endpoint: '/test',
|
|
280
|
+
version: 'v1',
|
|
281
|
+
tokenAuth: true,
|
|
282
|
+
run: (services, inputs, next) => {
|
|
283
|
+
next(200, { message: 'Success' });
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
`;
|
|
287
|
+
require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
|
|
288
|
+
|
|
289
|
+
const api = new Api(config);
|
|
290
|
+
await api.init();
|
|
291
|
+
|
|
292
|
+
// Test that default JWT validation still works
|
|
293
|
+
const token = Auth.generateToken({ userId: 'legacy-user' }, { secretKey: 'test-secret' });
|
|
294
|
+
const decoded = Auth.verifyToken(token, 'test-secret');
|
|
295
|
+
assert.strictEqual(decoded.userId, 'legacy-user');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|