jxp 2.16.0 → 3.0.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/.vscode/launch.json +7 -2
- package/bin/server.js +53 -6
- package/docs/authentication.md +1 -1
- package/libs/login.js +9 -5
- package/libs/security.js +24 -24
- package/libs/ws.js +27 -4
- package/package.json +3 -3
- package/test/auth.js +190 -0
- package/test/init.js +6 -6
package/.vscode/launch.json
CHANGED
|
@@ -19,11 +19,16 @@
|
|
|
19
19
|
"-u",
|
|
20
20
|
"tdd",
|
|
21
21
|
"--timeout",
|
|
22
|
-
"
|
|
22
|
+
"30000",
|
|
23
23
|
"--colors",
|
|
24
|
+
"--exit",
|
|
25
|
+
"--bail",
|
|
24
26
|
"${workspaceFolder}/test"
|
|
25
27
|
],
|
|
26
|
-
"internalConsoleOptions": "openOnSessionStart"
|
|
28
|
+
"internalConsoleOptions": "openOnSessionStart",
|
|
29
|
+
"env": {
|
|
30
|
+
"NODE_ENV": "test"
|
|
31
|
+
}
|
|
27
32
|
}
|
|
28
33
|
]
|
|
29
34
|
}
|
package/bin/server.js
CHANGED
|
@@ -26,9 +26,9 @@ config.callbacks = {
|
|
|
26
26
|
// console.log("Delete callback");
|
|
27
27
|
// }
|
|
28
28
|
|
|
29
|
-
post: function() {},
|
|
30
|
-
put: function() {},
|
|
31
|
-
delete: function() {}
|
|
29
|
+
post: function () { },
|
|
30
|
+
put: function () { },
|
|
31
|
+
delete: function () { }
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
config.pre_hooks = {
|
|
@@ -56,7 +56,19 @@ config.pre_hooks = {
|
|
|
56
56
|
mongoose.Promise = Promise;
|
|
57
57
|
if (!config.mongo) config.mongo = {};
|
|
58
58
|
if (!config.mongo.options) config.mongo.options = {};
|
|
59
|
-
const mongo_options = Object.assign(config.mongo.options, {
|
|
59
|
+
const mongo_options = Object.assign(config.mongo.options, {
|
|
60
|
+
useNewUrlParser: true,
|
|
61
|
+
useUnifiedTopology: true,
|
|
62
|
+
serverSelectionTimeoutMS: 10000,
|
|
63
|
+
socketTimeoutMS: 45000,
|
|
64
|
+
maxPoolSize: process.env.NODE_ENV === 'test' ? 20 : 50,
|
|
65
|
+
minPoolSize: process.env.NODE_ENV === 'test' ? 5 : 10,
|
|
66
|
+
maxIdleTimeMS: 30000,
|
|
67
|
+
connectTimeoutMS: 10000,
|
|
68
|
+
heartbeatFrequencyMS: 10000,
|
|
69
|
+
retryWrites: true,
|
|
70
|
+
retryReads: true
|
|
71
|
+
});
|
|
60
72
|
|
|
61
73
|
const connection_string = require("../libs/connection_string");
|
|
62
74
|
console.log(`Connecting to ${connection_string}`);
|
|
@@ -65,18 +77,53 @@ mongoose.connect(connection_string, mongo_options);
|
|
|
65
77
|
const db = mongoose.connection;
|
|
66
78
|
|
|
67
79
|
// mongodb error
|
|
68
|
-
db.on('error',
|
|
80
|
+
db.on('error', (err) => {
|
|
81
|
+
console.error('MongoDB connection error:', err);
|
|
82
|
+
if (err.name === 'MongoNetworkError') {
|
|
83
|
+
// Handle network errors
|
|
84
|
+
console.error('Network error occurred. Attempting to reconnect...');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
69
87
|
|
|
70
88
|
// mongodb connection open
|
|
71
89
|
db.once('open', () => {
|
|
72
90
|
console.log(`Connected to Mongo at: ${new Date()}`);
|
|
91
|
+
console.log('Connection pool size:', mongoose.connection.base.connections.length);
|
|
73
92
|
});
|
|
74
93
|
|
|
94
|
+
mongoose.connection.on('connected', () => {
|
|
95
|
+
console.log('Mongoose connected');
|
|
96
|
+
console.log('Connection pool size:', mongoose.connection.base.connections.length);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
mongoose.connection.on('disconnected', () => {
|
|
100
|
+
console.log('Mongoose disconnected');
|
|
101
|
+
// Attempt to reconnect after a delay
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
mongoose.connect(connection_string, mongo_options).catch(err => {
|
|
104
|
+
console.error('Failed to reconnect to MongoDB:', err);
|
|
105
|
+
});
|
|
106
|
+
}, 5000);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
mongoose.connection.on('error', (err) => {
|
|
110
|
+
console.error('Mongoose connection error:', err);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Monitor connection pool
|
|
114
|
+
setInterval(() => {
|
|
115
|
+
const poolSize = mongoose.connection.base.connections.length;
|
|
116
|
+
const maxPoolSize = mongo_options.maxPoolSize;
|
|
117
|
+
if (poolSize > maxPoolSize * 0.8) {
|
|
118
|
+
console.warn(`Connection pool is at ${Math.round((poolSize / maxPoolSize) * 100)}% capacity`);
|
|
119
|
+
}
|
|
120
|
+
}, 30000);
|
|
121
|
+
|
|
75
122
|
var server = new JXP(config);
|
|
76
123
|
|
|
77
124
|
let port = process.env.NODE_DOCKER_PORT || process.env.PORT || config.port || 4001;
|
|
78
125
|
if (process.env.NODE_ENV === "test") port = 4005;
|
|
79
|
-
server.listen(port, function() {
|
|
126
|
+
server.listen(port, function () {
|
|
80
127
|
console.log('%s listening at %s', `${pkg.name} v${pkg.version}`, server.url);
|
|
81
128
|
console.log(`Mongoose version ${mongoose.version}`);
|
|
82
129
|
});
|
package/docs/authentication.md
CHANGED
|
@@ -179,5 +179,5 @@ Header: `Authorization: Bearer <your bearer token>`
|
|
|
179
179
|
|
|
180
180
|
### API Key
|
|
181
181
|
|
|
182
|
-
The API Key is a permanent key that doesn't expire. It can be used by adding `?apikey=<apikey>` to the end of any request, or sending `
|
|
182
|
+
The API Key is a permanent key that doesn't expire. It can be used by adding `?apikey=<apikey>` to the end of any request, or sending `x-api-key: <apikey>` in the header.
|
|
183
183
|
|
package/libs/login.js
CHANGED
|
@@ -65,7 +65,7 @@ const logout = async (req, res) => {
|
|
|
65
65
|
if (!res.user) throw new errors.ForbiddenError("You don't seem to be logged in");
|
|
66
66
|
await security.revokeToken(res.user._id);
|
|
67
67
|
res.send({ status: "ok", message: "User logged out" });
|
|
68
|
-
} catch(err) {
|
|
68
|
+
} catch (err) {
|
|
69
69
|
console.error(err);
|
|
70
70
|
if (err.code) throw err;
|
|
71
71
|
throw new errors.InternalServerError(err.toString());
|
|
@@ -148,13 +148,17 @@ const login = async (req, res) => {
|
|
|
148
148
|
}
|
|
149
149
|
if ((!password) || (!email)) {
|
|
150
150
|
console.error(new Date(), "Missing email or password parameters");
|
|
151
|
-
|
|
151
|
+
throw new errors.ForbiddenError("Missing email or password parameters");
|
|
152
152
|
}
|
|
153
153
|
try {
|
|
154
154
|
const user = await User.findOne({ email });
|
|
155
|
-
if (!user)
|
|
155
|
+
if (!user) {
|
|
156
|
+
console.error(new Date(), `Authentication failed - user not found`, ip, email);
|
|
157
|
+
throw new errors.ForbiddenError("Incorrect email or password");
|
|
158
|
+
}
|
|
156
159
|
if (!(await bcrypt.compare(password, user.password))) {
|
|
157
|
-
|
|
160
|
+
console.error(new Date(), `Authentication failed - invalid password`, ip, email);
|
|
161
|
+
throw new errors.ForbiddenError("Incorrect email or password");
|
|
158
162
|
}
|
|
159
163
|
const token = await security.refreshToken(user._id);
|
|
160
164
|
const refreshtoken = await security.ensureRefreshToken(user._id);
|
|
@@ -171,7 +175,7 @@ const login = async (req, res) => {
|
|
|
171
175
|
} catch (err) {
|
|
172
176
|
console.error(new Date(), `Authentication failed`, ip, err);
|
|
173
177
|
if (err.code) throw err;
|
|
174
|
-
|
|
178
|
+
throw new errors.ForbiddenError("Incorrect email or password");
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
181
|
|
package/libs/security.js
CHANGED
|
@@ -9,7 +9,7 @@ var User = null;
|
|
|
9
9
|
var RefreshToken = null;
|
|
10
10
|
var provider = "https://api.workspaceman.nl";
|
|
11
11
|
|
|
12
|
-
const init = function(config) {
|
|
12
|
+
const init = function (config) {
|
|
13
13
|
APIKey = require(path.join(config.model_dir, "apikey_model"));
|
|
14
14
|
Groups = require(path.join(config.model_dir, "usergroups_model.js"));
|
|
15
15
|
User = require(path.join(config.model_dir, "user_model"));
|
|
@@ -18,7 +18,7 @@ const init = function(config) {
|
|
|
18
18
|
if (config.url) provider = config.url;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
const basicAuthData = function(req) {
|
|
21
|
+
const basicAuthData = function (req) {
|
|
22
22
|
if (!req.headers.authorization) {
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
@@ -34,19 +34,19 @@ const basicAuthData = function(req) {
|
|
|
34
34
|
const basicAuth = async ba => {
|
|
35
35
|
try {
|
|
36
36
|
if (!Array.isArray(ba) || ba.length !== 2) {
|
|
37
|
-
throw("Basic Auth incorrectly formatted");
|
|
37
|
+
throw ("Basic Auth incorrectly formatted");
|
|
38
38
|
}
|
|
39
39
|
var email = ba[0];
|
|
40
40
|
var password = ba[1];
|
|
41
41
|
const user = await User.findOne({ email }).exec();
|
|
42
42
|
if (!user) {
|
|
43
|
-
throw(new Date(), `Incorrect username or password for ${email}`);
|
|
43
|
+
throw (new Date(), `Incorrect username or password for ${email}`);
|
|
44
44
|
}
|
|
45
45
|
if (!await bcrypt.compare(password, user.password)) {
|
|
46
|
-
throw(`Incorrect username or password for ${email}`);
|
|
46
|
+
throw (`Incorrect username or password for ${email}`);
|
|
47
47
|
}
|
|
48
48
|
return user;
|
|
49
|
-
} catch(err) {
|
|
49
|
+
} catch (err) {
|
|
50
50
|
console.error(new Date(), err);
|
|
51
51
|
throw err;
|
|
52
52
|
}
|
|
@@ -66,13 +66,13 @@ const bearerAuthData = req => {
|
|
|
66
66
|
|
|
67
67
|
const bearerAuth = async t => {
|
|
68
68
|
try {
|
|
69
|
-
if (!t) throw("Token invalid");
|
|
69
|
+
if (!t) throw ("Token invalid");
|
|
70
70
|
const token = await Token.findOne({ access_token: t, provider }).exec();
|
|
71
71
|
if (!token) {
|
|
72
|
-
throw(`Token ${t} not found`);
|
|
72
|
+
throw (`Token ${t} not found`);
|
|
73
73
|
}
|
|
74
74
|
if (!tokenIsValid(token)) {
|
|
75
|
-
throw(`Token is no loger valid`);
|
|
75
|
+
throw (`Token is no loger valid`);
|
|
76
76
|
}
|
|
77
77
|
const user = await User.findOne({ _id: token.user_id }).exec();
|
|
78
78
|
if (!user) {
|
|
@@ -87,13 +87,13 @@ const bearerAuth = async t => {
|
|
|
87
87
|
|
|
88
88
|
const apiKeyAuth = async apikey => {
|
|
89
89
|
try {
|
|
90
|
-
if (!apikey) throw("Missing apikey");
|
|
90
|
+
if (!apikey) throw ("Missing apikey");
|
|
91
91
|
const result = await APIKey.findOne({ apikey });
|
|
92
|
-
if (!result) throw("Could not find apikey");
|
|
92
|
+
if (!result) throw ("Could not find apikey");
|
|
93
93
|
const user = User.findOne({ _id: result.user_id });
|
|
94
|
-
if (!user) throw("Could not find user associated to apikey");
|
|
94
|
+
if (!user) throw ("Could not find user associated to apikey");
|
|
95
95
|
return user;
|
|
96
|
-
} catch(err) {
|
|
96
|
+
} catch (err) {
|
|
97
97
|
console.error(new Date(), err);
|
|
98
98
|
throw err;
|
|
99
99
|
}
|
|
@@ -104,7 +104,7 @@ const getGroups = async user_id => {
|
|
|
104
104
|
const userGroup = await Groups.findOne({ user_id });
|
|
105
105
|
var groups = userGroup && userGroup.groups ? userGroup.groups : [];
|
|
106
106
|
return groups;
|
|
107
|
-
} catch(err) {
|
|
107
|
+
} catch (err) {
|
|
108
108
|
console.error(new Date(), err);
|
|
109
109
|
throw err;
|
|
110
110
|
}
|
|
@@ -126,7 +126,7 @@ const generateApiKey = async user_id => {
|
|
|
126
126
|
apikey.apikey = randToken.generate(16);
|
|
127
127
|
await apikey.save();
|
|
128
128
|
return apikey;
|
|
129
|
-
} catch(err) {
|
|
129
|
+
} catch (err) {
|
|
130
130
|
console.error(new Date(), err);
|
|
131
131
|
throw err;
|
|
132
132
|
}
|
|
@@ -208,10 +208,10 @@ const revokeRefreshToken = async user_id => {
|
|
|
208
208
|
|
|
209
209
|
const refresh = async (req, res) => {
|
|
210
210
|
try {
|
|
211
|
-
if(req.headers.authorization && req.headers.authorization.trim().toLowerCase().indexOf("bearer") === 0) {
|
|
211
|
+
if (req.headers.authorization && req.headers.authorization.trim().toLowerCase().indexOf("bearer") === 0) {
|
|
212
212
|
const refresh_token = await RefreshToken.findOne({ refresh_token: bearerAuthData(req) }).exec();
|
|
213
|
-
if (!refresh_token) throw("Refresh token not found");
|
|
214
|
-
if (!tokenIsValid(refresh_token)) throw("Refresh token has expired");
|
|
213
|
+
if (!refresh_token) throw ("Refresh token not found");
|
|
214
|
+
if (!tokenIsValid(refresh_token)) throw ("Refresh token has expired");
|
|
215
215
|
const user_id = refresh_token.user_id;
|
|
216
216
|
const token = await refreshToken(user_id);
|
|
217
217
|
await revokeRefreshToken(user_id);
|
|
@@ -224,7 +224,7 @@ const refresh = async (req, res) => {
|
|
|
224
224
|
refresh_token_expires: tokenExpires(new_refresh_token)
|
|
225
225
|
});
|
|
226
226
|
} else {
|
|
227
|
-
throw("Missing refresh token")
|
|
227
|
+
throw ("Missing refresh token")
|
|
228
228
|
}
|
|
229
229
|
} catch (err) {
|
|
230
230
|
console.error(err);
|
|
@@ -242,7 +242,7 @@ const login = async (req, res) => {
|
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
244
|
res = Object.assign(res, authenticate_result);
|
|
245
|
-
} catch(err) {
|
|
245
|
+
} catch (err) {
|
|
246
246
|
console.error(err);
|
|
247
247
|
if (err.code) throw err;
|
|
248
248
|
throw new errors.ForbiddenError(err.toString());
|
|
@@ -264,7 +264,7 @@ const authenticate = async req => {
|
|
|
264
264
|
user = await apiKeyAuth(req.query.apikey);
|
|
265
265
|
} else if (req.headers["X-API-Key"] || req.headers["x-api-key"]) {
|
|
266
266
|
// API Key
|
|
267
|
-
user = await apiKeyAuth(req.
|
|
267
|
+
user = await apiKeyAuth(req.headers["X-API-Key"] || req.headers["x-api-key"])
|
|
268
268
|
} else {
|
|
269
269
|
throw ("Could not find any way to authenticate");
|
|
270
270
|
}
|
|
@@ -278,7 +278,7 @@ const authenticate = async req => {
|
|
|
278
278
|
username: user.email,
|
|
279
279
|
user
|
|
280
280
|
}
|
|
281
|
-
|
|
281
|
+
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
const auth = async (req, res) => {
|
|
@@ -303,7 +303,7 @@ const auth = async (req, res) => {
|
|
|
303
303
|
throw new errors.InternalServerError(`Unsupported operation: ${req.method}`);
|
|
304
304
|
}
|
|
305
305
|
return await check_perms(res.user, res.groups, req.Model, method, req.params.item_id);
|
|
306
|
-
} catch(err) {
|
|
306
|
+
} catch (err) {
|
|
307
307
|
console.error(err);
|
|
308
308
|
if (err.code) throw err;
|
|
309
309
|
throw new errors.ForbiddenError(err.toString());
|
|
@@ -355,7 +355,7 @@ const check_perms = async (user, groups, model, method, item_id) => {
|
|
|
355
355
|
}
|
|
356
356
|
//Group check
|
|
357
357
|
for (let group of groups) {
|
|
358
|
-
if (perms[group] && perms[group].includes(method)
|
|
358
|
+
if (perms[group] && perms[group].includes(method)) {
|
|
359
359
|
// console.log("Matched permission '" + group + "':" + method);
|
|
360
360
|
return;
|
|
361
361
|
}
|
package/libs/ws.js
CHANGED
|
@@ -235,24 +235,47 @@ class WSClient {
|
|
|
235
235
|
wss.on('connection', function connection(ws) {
|
|
236
236
|
try {
|
|
237
237
|
const client = new WSClient(ws);
|
|
238
|
-
|
|
238
|
+
const startTime = Date.now();
|
|
239
|
+
console.log(`New WebSocket connection established at ${new Date()}`);
|
|
240
|
+
|
|
239
241
|
ws.on('message', async function message(msg) {
|
|
242
|
+
const messageStartTime = Date.now();
|
|
240
243
|
try {
|
|
241
|
-
console.log(`Received message from ${client.id}`);
|
|
244
|
+
console.log(`Received message from ${client.id} at ${new Date()}`);
|
|
242
245
|
const result = await client.receive(msg);
|
|
243
246
|
ws.send(JSON.stringify(result));
|
|
247
|
+
console.log(`Message processed in ${Date.now() - messageStartTime}ms`);
|
|
244
248
|
} catch (err) {
|
|
245
249
|
console.error("message error", err);
|
|
250
|
+
console.error(`Message processing failed after ${Date.now() - messageStartTime}ms`);
|
|
246
251
|
}
|
|
247
252
|
});
|
|
253
|
+
|
|
248
254
|
ws.on('close', function close() {
|
|
255
|
+
const connectionDuration = Date.now() - startTime;
|
|
249
256
|
if (client.user) {
|
|
250
|
-
console.log(`
|
|
257
|
+
console.log(`Connection closed for ${client.user.name} <${client.user.email}> after ${connectionDuration}ms`);
|
|
251
258
|
} else {
|
|
252
|
-
console.log(`
|
|
259
|
+
console.log(`Anonymous connection closed after ${connectionDuration}ms`);
|
|
253
260
|
}
|
|
254
261
|
client.close();
|
|
255
262
|
});
|
|
263
|
+
|
|
264
|
+
// Add ping/pong monitoring
|
|
265
|
+
const pingInterval = setInterval(() => {
|
|
266
|
+
if (ws.readyState === ws.OPEN) {
|
|
267
|
+
const pingStart = Date.now();
|
|
268
|
+
ws.ping();
|
|
269
|
+
ws.once('pong', () => {
|
|
270
|
+
const latency = Date.now() - pingStart;
|
|
271
|
+
if (latency > 1000) { // Alert if latency > 1 second
|
|
272
|
+
console.warn(`High WebSocket latency detected: ${latency}ms`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}, 30000);
|
|
277
|
+
|
|
278
|
+
ws.on('close', () => clearInterval(pingInterval));
|
|
256
279
|
} catch (err) {
|
|
257
280
|
console.error("connection error", err);
|
|
258
281
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jxp",
|
|
3
3
|
"description:": "An opinionated RESTful API library based on Mongoose and Restify. Make an API by just writing Mongoose models.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "libs/jxp.js",
|
|
7
7
|
"scripts": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"bcryptjs": "^3.0.2",
|
|
34
34
|
"commander": "^13.1.0",
|
|
35
35
|
"config": "^3.3.12",
|
|
36
|
-
"dotenv": "^16.
|
|
36
|
+
"dotenv": "^16.5.0",
|
|
37
37
|
"glob": "11.0.1",
|
|
38
38
|
"js-yaml": "4.1.0",
|
|
39
39
|
"json2csv": "^5.0.7",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"mongoose-friendly": "^0.1.4",
|
|
47
47
|
"morgan": "^1.10.0",
|
|
48
48
|
"node-cache": "^5.1.2",
|
|
49
|
-
"nodemailer": "^6.10.
|
|
49
|
+
"nodemailer": "^6.10.1",
|
|
50
50
|
"nodemailer-smtp-transport": "^2.7.4",
|
|
51
51
|
"path": "^0.12.7",
|
|
52
52
|
"pug": "^3.0.3",
|
package/test/auth.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
process.env.NODE_ENV = 'test';
|
|
2
|
+
|
|
3
|
+
var chai = require('chai');
|
|
4
|
+
var chaiHttp = require('chai-http');
|
|
5
|
+
|
|
6
|
+
var init = require("./init");
|
|
7
|
+
var server = require("../bin/server");
|
|
8
|
+
|
|
9
|
+
chai.use(chaiHttp);
|
|
10
|
+
|
|
11
|
+
describe('Authentication Tests', () => {
|
|
12
|
+
before(async function () {
|
|
13
|
+
// First empty collections
|
|
14
|
+
await init.empty_user_collections();
|
|
15
|
+
console.log("Emptied collections");
|
|
16
|
+
|
|
17
|
+
// Then run setup
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
chai.request(server)
|
|
20
|
+
.post("/setup")
|
|
21
|
+
.send({ email: init.email, password: init.password })
|
|
22
|
+
.end((err, res) => {
|
|
23
|
+
if (err) return reject(err);
|
|
24
|
+
res.should.have.status(200);
|
|
25
|
+
res.body.status.should.equal('success');
|
|
26
|
+
console.log("Setup complete with email:", init.email);
|
|
27
|
+
resolve();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
var apikey = null;
|
|
33
|
+
var token = null;
|
|
34
|
+
var refresh_token = null;
|
|
35
|
+
var user_id = null;
|
|
36
|
+
|
|
37
|
+
describe("Login", () => {
|
|
38
|
+
it("should login with valid credentials", (done) => {
|
|
39
|
+
chai.request(server)
|
|
40
|
+
.post("/login")
|
|
41
|
+
.send({ email: init.email, password: init.password })
|
|
42
|
+
.end((err, res) => {
|
|
43
|
+
res.should.have.status(200);
|
|
44
|
+
res.body.should.have.property('user_id');
|
|
45
|
+
res.body.should.have.property('apikey');
|
|
46
|
+
res.body.should.have.property('token');
|
|
47
|
+
res.body.should.have.property('token_expires');
|
|
48
|
+
res.body.should.have.property('refresh_token');
|
|
49
|
+
res.body.should.have.property('refresh_token_expires');
|
|
50
|
+
res.body.should.have.property('provider');
|
|
51
|
+
apikey = res.body.apikey;
|
|
52
|
+
token = res.body.token;
|
|
53
|
+
user_id = res.body.user_id;
|
|
54
|
+
refresh_token = res.body.refresh_token;
|
|
55
|
+
done();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should fail with invalid credentials", (done) => {
|
|
60
|
+
console.log("Attempting login with invalid password for:", init.email);
|
|
61
|
+
chai.request(server)
|
|
62
|
+
.post("/login")
|
|
63
|
+
.send({ email: init.email, password: "wrongpassword" })
|
|
64
|
+
.end((err, res) => {
|
|
65
|
+
if (err) return done(err);
|
|
66
|
+
console.log("Response status:", res.status);
|
|
67
|
+
console.log("Response body:", res.body);
|
|
68
|
+
res.should.have.status(403);
|
|
69
|
+
done();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("Token Authentication", () => {
|
|
75
|
+
it("should authenticate with valid token", (done) => {
|
|
76
|
+
chai.request(server)
|
|
77
|
+
.get("/api/user")
|
|
78
|
+
.set("Authorization", `Bearer ${token}`)
|
|
79
|
+
.end((err, res) => {
|
|
80
|
+
if (err) return done(err);
|
|
81
|
+
res.should.have.status(200);
|
|
82
|
+
res.body.data.should.be.an('array');
|
|
83
|
+
done();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should fail with invalid token", (done) => {
|
|
88
|
+
chai.request(server)
|
|
89
|
+
.get("/api/user")
|
|
90
|
+
.set("Authorization", "Bearer invalidtoken")
|
|
91
|
+
.end((err, res) => {
|
|
92
|
+
if (err) return done(err);
|
|
93
|
+
res.should.have.status(403);
|
|
94
|
+
done();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("API Key Authentication", () => {
|
|
100
|
+
it("should authenticate with valid API key in header", (done) => {
|
|
101
|
+
chai.request(server)
|
|
102
|
+
.get("/api/user")
|
|
103
|
+
.set("X-API-Key", apikey)
|
|
104
|
+
.end((err, res) => {
|
|
105
|
+
if (err) return done(err);
|
|
106
|
+
res.should.have.status(200);
|
|
107
|
+
res.body.data.should.be.an('array');
|
|
108
|
+
done();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should authenticate with valid API key in query", (done) => {
|
|
113
|
+
chai.request(server)
|
|
114
|
+
.get(`/api/user?apikey=${apikey}`)
|
|
115
|
+
.end((err, res) => {
|
|
116
|
+
if (err) return done(err);
|
|
117
|
+
res.should.have.status(200);
|
|
118
|
+
res.body.data.should.be.an('array');
|
|
119
|
+
done();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should fail with invalid API key", (done) => {
|
|
124
|
+
chai.request(server)
|
|
125
|
+
.get("/api/user")
|
|
126
|
+
.set("X-API-Key", "invalidapikey")
|
|
127
|
+
.end((err, res) => {
|
|
128
|
+
if (err) return done(err);
|
|
129
|
+
res.should.have.status(403);
|
|
130
|
+
done();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("Refresh Token", () => {
|
|
136
|
+
it("should refresh token successfully", (done) => {
|
|
137
|
+
chai.request(server)
|
|
138
|
+
.post("/refresh")
|
|
139
|
+
.set("Authorization", `Bearer ${refresh_token}`)
|
|
140
|
+
.end((err, res) => {
|
|
141
|
+
if (err) return done(err);
|
|
142
|
+
res.should.have.status(200);
|
|
143
|
+
res.body.should.have.property('token');
|
|
144
|
+
res.body.should.have.property('token_expires');
|
|
145
|
+
res.body.should.have.property('refresh_token');
|
|
146
|
+
res.body.should.have.property('refresh_token_expires');
|
|
147
|
+
res.body.token.should.not.eql(token);
|
|
148
|
+
res.body.refresh_token.should.not.eql(refresh_token);
|
|
149
|
+
token = res.body.token;
|
|
150
|
+
refresh_token = res.body.refresh_token;
|
|
151
|
+
done();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should fail with invalid refresh token", (done) => {
|
|
156
|
+
chai.request(server)
|
|
157
|
+
.post("/refresh")
|
|
158
|
+
.set("Authorization", "Bearer invalidrefreshtoken")
|
|
159
|
+
.end((err, res) => {
|
|
160
|
+
if (err) return done(err);
|
|
161
|
+
res.should.have.status(403);
|
|
162
|
+
done();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("Logout", () => {
|
|
168
|
+
it("should logout successfully", (done) => {
|
|
169
|
+
chai.request(server)
|
|
170
|
+
.get("/login/logout")
|
|
171
|
+
.set("Authorization", `Bearer ${token}`)
|
|
172
|
+
.end((err, res) => {
|
|
173
|
+
if (err) return done(err);
|
|
174
|
+
res.should.have.status(200);
|
|
175
|
+
done();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should not be able to use token after logout", (done) => {
|
|
180
|
+
chai.request(server)
|
|
181
|
+
.get("/api/user")
|
|
182
|
+
.set("Authorization", `Bearer ${token}`)
|
|
183
|
+
.end((err, res) => {
|
|
184
|
+
if (err) return done(err);
|
|
185
|
+
res.should.have.status(403);
|
|
186
|
+
done();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
package/test/init.js
CHANGED
|
@@ -15,9 +15,9 @@ const security = require("../libs/security");
|
|
|
15
15
|
const empty = async model => {
|
|
16
16
|
try {
|
|
17
17
|
await model.deleteMany({});
|
|
18
|
-
} catch(err) {
|
|
18
|
+
} catch (err) {
|
|
19
19
|
console.error(err);
|
|
20
|
-
throw(err);
|
|
20
|
+
throw (err);
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -44,9 +44,9 @@ const init = async () => {
|
|
|
44
44
|
await post(User, { name: "Admin User", email: admin_email, password: security.encPassword(admin_password), urlid: "admin-user", admin: true });
|
|
45
45
|
await post(User, { name: "Test User", email, password: security.encPassword(password), urlid: "test-user" });
|
|
46
46
|
return true;
|
|
47
|
-
} catch(err) {
|
|
47
|
+
} catch (err) {
|
|
48
48
|
console.error(err);
|
|
49
|
-
throw(err);
|
|
49
|
+
throw (err);
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
@@ -55,9 +55,9 @@ const empty_user_collections = async () => {
|
|
|
55
55
|
await empty(User);
|
|
56
56
|
await empty(Apikey);
|
|
57
57
|
await empty(Test);
|
|
58
|
-
} catch(err) {
|
|
58
|
+
} catch (err) {
|
|
59
59
|
console.error(err);
|
|
60
|
-
throw(err);
|
|
60
|
+
throw (err);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|