@stanlemon/server-with-auth 0.2.12 → 0.3.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 +48 -0
- package/app.js +37 -4
- package/package.json +7 -3
- package/src/constants.js +59 -0
- package/src/createAppServer.js +71 -29
- package/src/data/knex-user-dao.js +142 -0
- package/src/data/lowdb-user-dao.js +79 -49
- package/src/data/user-dao.js +38 -11
- package/src/data/user-dao.test.js +156 -0
- package/src/index.js +5 -4
- package/src/routes/auth.js +221 -48
- package/src/routes/auth.test.js +204 -49
- package/src/schema/index.js +70 -0
- package/src/utilities/checkSchemas.js +25 -0
- package/src/utilities/checkUserDao.js +7 -0
- package/test.http +62 -4
- package/src/data/lowdb-user-dao.test.js +0 -111
- package/src/schema/user.js +0 -8
package/src/routes/auth.js
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { isEmpty, omit } from "lodash-es";
|
|
2
3
|
import { Router } from "express";
|
|
4
|
+
import passport from "passport";
|
|
3
5
|
import jwt from "jsonwebtoken";
|
|
4
|
-
import {
|
|
5
|
-
schemaHandler,
|
|
6
|
-
formatOutput,
|
|
7
|
-
BadRequestException,
|
|
8
|
-
} from "@stanlemon/server";
|
|
6
|
+
import { schemaHandler, formatOutput } from "@stanlemon/server";
|
|
9
7
|
import checkAuth from "../checkAuth.js";
|
|
10
|
-
import
|
|
8
|
+
import checkUserDao from "../utilities/checkUserDao.js";
|
|
9
|
+
import checkSchemas from "../utilities/checkSchemas.js";
|
|
10
|
+
import { HIDDEN_FIELDS, ROUTES, EVENTS } from "../constants.js";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Create express routes for authentication operations.
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {string[]} options.secret Route patterns to protect by authentication.
|
|
16
|
+
* @param {Object.<string, Joi.Schema>} options.schemas Object map of routes to schema for validating route inputs.
|
|
17
|
+
* @param {import("../data/user-dao.js").default} options.dao Dao to use for various user operations
|
|
18
|
+
* @param {EventEmitter} options.eventEmitter Event emitter that can be used to hook into user operations
|
|
19
|
+
* @param {number} options.jwtExpireInMinutes Number of minutes before a JWT token expires
|
|
20
|
+
* @returns {Express.Router}
|
|
21
|
+
*/
|
|
12
22
|
/* eslint-disable max-lines-per-function */
|
|
13
|
-
export default function authRoutes({
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
export default function authRoutes({
|
|
24
|
+
secret,
|
|
25
|
+
schemas,
|
|
26
|
+
dao,
|
|
27
|
+
eventEmitter,
|
|
28
|
+
jwtExpireInMinutes,
|
|
29
|
+
}) {
|
|
30
|
+
checkUserDao(dao);
|
|
31
|
+
checkSchemas(schemas);
|
|
32
|
+
|
|
33
|
+
if (!(eventEmitter instanceof EventEmitter)) {
|
|
34
|
+
throw new Error("The eventEmitter object must be of type EventEmitter.");
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
const router = Router();
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
const
|
|
39
|
+
const makeJwtToken = (user) => {
|
|
40
|
+
const token = jwt.sign({ ...user }, secret, {
|
|
41
|
+
expiresIn: 60 * jwtExpireInMinutes,
|
|
42
|
+
});
|
|
43
|
+
return token;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
router.get(ROUTES.SESSION, checkAuth(), async (req, res) => {
|
|
47
|
+
const userId = req.user.id;
|
|
22
48
|
|
|
23
49
|
if (!userId) {
|
|
24
50
|
res.status(401).json({
|
|
@@ -36,38 +62,56 @@ export default function authRoutes({ secret, schema, dao }) {
|
|
|
36
62
|
user: false,
|
|
37
63
|
});
|
|
38
64
|
} else {
|
|
39
|
-
const token =
|
|
40
|
-
res.status(200).json({ token, user: formatOutput(user,
|
|
65
|
+
const token = makeJwtToken(user);
|
|
66
|
+
res.status(200).json({ token, user: formatOutput(user, HIDDEN_FIELDS) });
|
|
41
67
|
}
|
|
42
68
|
});
|
|
43
69
|
|
|
44
|
-
router.post(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
router.post(ROUTES.LOGIN, (req, res, next) => {
|
|
71
|
+
// Customizing the login so we can send a proper JSON response when unable to login
|
|
72
|
+
passport.authenticate("local", (err, user, info) => {
|
|
73
|
+
if (err) {
|
|
74
|
+
return next(err);
|
|
75
|
+
}
|
|
76
|
+
if (!user) {
|
|
77
|
+
return res.status(401).json({
|
|
78
|
+
success: false,
|
|
79
|
+
message: "Incorrect username or password.",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
49
82
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
83
|
+
return req.logIn(user, (err) => {
|
|
84
|
+
if (err) {
|
|
85
|
+
return next(err);
|
|
86
|
+
}
|
|
56
87
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
88
|
+
return dao
|
|
89
|
+
.updateUser(user.id, {
|
|
90
|
+
last_logged_in: new Date(),
|
|
91
|
+
})
|
|
92
|
+
.then((update) => {
|
|
93
|
+
const token = makeJwtToken(user);
|
|
60
94
|
|
|
61
|
-
|
|
95
|
+
eventEmitter.emit(EVENTS.USER_LOGIN, omit(user, ["password"]));
|
|
62
96
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
return res.json({
|
|
98
|
+
token,
|
|
99
|
+
user: formatOutput(update, HIDDEN_FIELDS),
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
})(req, res, next);
|
|
67
104
|
});
|
|
68
105
|
|
|
69
|
-
router.get(
|
|
70
|
-
|
|
106
|
+
router.get(ROUTES.LOGOUT, (req, res) => {
|
|
107
|
+
// This will happen if you are only using the JWT strategy
|
|
108
|
+
if (req.logout !== undefined) {
|
|
109
|
+
req.logout();
|
|
110
|
+
} else {
|
|
111
|
+
console.warn("Logout attempted, but there is no logout function.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
eventEmitter.emit(EVENTS.USER_LOGOUT, req.user.id);
|
|
71
115
|
|
|
72
116
|
return res.status(401).json({
|
|
73
117
|
token: false,
|
|
@@ -76,33 +120,38 @@ export default function authRoutes({ secret, schema, dao }) {
|
|
|
76
120
|
});
|
|
77
121
|
|
|
78
122
|
router.post(
|
|
79
|
-
|
|
80
|
-
schemaHandler(
|
|
123
|
+
ROUTES.SIGNUP,
|
|
124
|
+
schemaHandler(schemas[ROUTES.SIGNUP], async (data, req, res) => {
|
|
81
125
|
const existing = await dao.getUserByUsername(data.username);
|
|
82
126
|
|
|
83
127
|
if (existing) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
res.status(400).json({
|
|
129
|
+
success: false,
|
|
130
|
+
message: "A user with this username already exists.",
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
87
133
|
}
|
|
88
134
|
|
|
89
|
-
const user = await dao.createUser(
|
|
135
|
+
const user = await dao.createUser({
|
|
136
|
+
...data,
|
|
137
|
+
});
|
|
90
138
|
|
|
91
139
|
if (isEmpty(user)) {
|
|
92
|
-
|
|
140
|
+
res.status(400).json({
|
|
141
|
+
success: false,
|
|
93
142
|
message: "An error has occurred",
|
|
94
|
-
};
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
95
145
|
}
|
|
96
146
|
|
|
97
|
-
|
|
98
|
-
// user.verification_token
|
|
147
|
+
eventEmitter.emit(EVENTS.USER_CREATED, omit(user, ["password"]));
|
|
99
148
|
|
|
100
|
-
const token =
|
|
101
|
-
return { token, user: formatOutput(user,
|
|
149
|
+
const token = makeJwtToken(user);
|
|
150
|
+
return { token, user: formatOutput(user, HIDDEN_FIELDS) };
|
|
102
151
|
})
|
|
103
152
|
);
|
|
104
153
|
|
|
105
|
-
router.get(
|
|
154
|
+
router.get(ROUTES.VERIFY, async (req, res) => {
|
|
106
155
|
const { token } = req.params;
|
|
107
156
|
|
|
108
157
|
const user = await dao.getUserByVerificationToken(token);
|
|
@@ -121,8 +170,132 @@ export default function authRoutes({ secret, schema, dao }) {
|
|
|
121
170
|
|
|
122
171
|
await dao.updateUser(user.id, { verified_date: new Date() });
|
|
123
172
|
|
|
173
|
+
eventEmitter.emit(EVENTS.USER_VERIFIED, omit(user, ["password"]));
|
|
174
|
+
|
|
124
175
|
return res.send({ success: true, message: "User verified!" });
|
|
125
176
|
});
|
|
126
177
|
|
|
178
|
+
router.get(ROUTES.USER, checkAuth(), async (req, res) => {
|
|
179
|
+
const userId = req.user.id;
|
|
180
|
+
|
|
181
|
+
if (!userId) {
|
|
182
|
+
res.status(401).json({
|
|
183
|
+
token: false,
|
|
184
|
+
user: false,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const user = await dao.getUserById(userId);
|
|
190
|
+
|
|
191
|
+
if (!user) {
|
|
192
|
+
res.status(401).json({
|
|
193
|
+
token: false,
|
|
194
|
+
user: false,
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
res.status(200).json(formatOutput(user, HIDDEN_FIELDS));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
router.put(
|
|
202
|
+
ROUTES.USER,
|
|
203
|
+
checkAuth(),
|
|
204
|
+
schemaHandler(schemas[ROUTES.USER], async (data, req, res) => {
|
|
205
|
+
const user = await dao.getUserById(req.user.id);
|
|
206
|
+
|
|
207
|
+
if (!user) {
|
|
208
|
+
res.status(404).json({
|
|
209
|
+
success: false,
|
|
210
|
+
message: "Not Found",
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const input = {
|
|
216
|
+
...omit(data, [
|
|
217
|
+
"id",
|
|
218
|
+
"created_at",
|
|
219
|
+
"last_updated",
|
|
220
|
+
"last_login",
|
|
221
|
+
"password",
|
|
222
|
+
"username",
|
|
223
|
+
"verification_token",
|
|
224
|
+
"verified_date",
|
|
225
|
+
]),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const updated = await dao.updateUser(user.id, input);
|
|
229
|
+
|
|
230
|
+
eventEmitter.emit(EVENTS.USER_UPDATED, omit(user, ["password"]));
|
|
231
|
+
|
|
232
|
+
res.status(200).json(formatOutput(updated, HIDDEN_FIELDS));
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
router.delete(ROUTES.USER, checkAuth(), async (req, res) => {
|
|
237
|
+
const deleted = await dao.deleteUser(req.user.id);
|
|
238
|
+
|
|
239
|
+
eventEmitter.emit(EVENTS.USER_DELETED, req.user.id, deleted);
|
|
240
|
+
|
|
241
|
+
res.status(200).json({ success: deleted });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
router.post(
|
|
245
|
+
ROUTES.PASSWORD,
|
|
246
|
+
checkAuth(),
|
|
247
|
+
schemaHandler(schemas[ROUTES.PASSWORD], async (data, req, res) => {
|
|
248
|
+
const currentUser = await dao.getUserById(req.user.id);
|
|
249
|
+
const user = await dao.getUserByUsernameAndPassword(
|
|
250
|
+
currentUser.username,
|
|
251
|
+
data.current_password // Reminder: Joi will switch the casing
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (!user) {
|
|
255
|
+
res.status(400).json({
|
|
256
|
+
success: false,
|
|
257
|
+
errors: {
|
|
258
|
+
current_password: "Current password is incorrect",
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const success = await dao.updateUser(user.id, {
|
|
265
|
+
password: data.password,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (success) {
|
|
269
|
+
eventEmitter.emit(EVENTS.USER_PASSWORD, omit(user, ["password"]));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { success: success };
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// TODO: This is a placeholder for a password reset request, you should implement the event handler to handle this.
|
|
277
|
+
router.get(
|
|
278
|
+
ROUTES.RESET,
|
|
279
|
+
schemaHandler(schemas[ROUTES.RESET], async (data) => {
|
|
280
|
+
const user = await dao.getUserByUsername(data.username);
|
|
281
|
+
|
|
282
|
+
eventEmitter.emit(EVENTS.USER_RESET_REQUESTED, omit(user, ["password"]));
|
|
283
|
+
|
|
284
|
+
return { success: true };
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// TODO: This is a placeholder for a password reset completion, you should implement the event handler to handle this.
|
|
289
|
+
router.post(
|
|
290
|
+
ROUTES.RESET,
|
|
291
|
+
schemaHandler(schemas[ROUTES.RESET], async (data) => {
|
|
292
|
+
const user = await dao.getUserByUsername(data.username);
|
|
293
|
+
|
|
294
|
+
eventEmitter.emit(EVENTS.USER_RESET_COMPLETED, omit(user, ["password"]));
|
|
295
|
+
|
|
296
|
+
return { success: true };
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
|
|
127
300
|
return router;
|
|
128
301
|
}
|
package/src/routes/auth.test.js
CHANGED
|
@@ -2,59 +2,62 @@
|
|
|
2
2
|
* @jest-environment node
|
|
3
3
|
*/
|
|
4
4
|
import request from "supertest";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import Joi from "joi";
|
|
6
|
+
import { v4 as uuidv4 } from "uuid";
|
|
7
|
+
import { createAppServer, createSchemas } from "../index.js";
|
|
8
|
+
import LowDBUserDao, { createInMemoryLowDb } from "../data/lowdb-user-dao.js";
|
|
7
9
|
|
|
8
10
|
// This suppresses a warning we don't need in tests
|
|
9
11
|
process.env.JWT_SECRET = "SECRET";
|
|
10
12
|
|
|
11
|
-
let dao = new LowDBUserDao(
|
|
13
|
+
let dao = new LowDBUserDao(createInMemoryLowDb());
|
|
12
14
|
|
|
13
15
|
// We want to explicitly test functionality we disable during testing
|
|
14
16
|
process.env.NODE_ENV = "override";
|
|
15
|
-
const app = createAppServer({
|
|
17
|
+
const app = createAppServer({
|
|
18
|
+
dao,
|
|
19
|
+
start: false,
|
|
20
|
+
schemas: createSchemas({
|
|
21
|
+
name: Joi.string().label("Name"),
|
|
22
|
+
email: Joi.string().email().label("Email"),
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
16
25
|
process.env.NODE_ENV = "test";
|
|
17
26
|
|
|
18
27
|
describe("/auth", () => {
|
|
19
|
-
let userId;
|
|
20
|
-
|
|
21
|
-
beforeAll(async () => {
|
|
22
|
-
// Reset our users database before each test
|
|
23
|
-
const user = await dao.createUser({
|
|
24
|
-
username: "test",
|
|
25
|
-
password: "test",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
userId = user.id;
|
|
29
|
-
});
|
|
30
|
-
|
|
31
28
|
// Disabling this linting rule because it is unaware of the supertest assertions
|
|
32
29
|
/* eslint-disable jest/expect-expect */
|
|
33
|
-
it("POST /
|
|
34
|
-
const username = "
|
|
30
|
+
it("POST /signup creates a user", async () => {
|
|
31
|
+
const username = "test" + uuidv4();
|
|
35
32
|
const password = "p@$$w0rd!";
|
|
33
|
+
const fullName = "Test User";
|
|
36
34
|
|
|
37
35
|
const data = {
|
|
38
36
|
username,
|
|
39
37
|
password,
|
|
38
|
+
fullName, // Not in the schema, should be discarded
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
await request(app)
|
|
43
|
-
.post("/auth/
|
|
42
|
+
.post("/auth/signup")
|
|
44
43
|
.set("Content-Type", "application/json")
|
|
45
44
|
.set("Accept", "application/json")
|
|
46
45
|
.send(data)
|
|
47
46
|
.expect(200)
|
|
48
47
|
.then((res) => {
|
|
48
|
+
expect(res.body.user.username).toEqual(username);
|
|
49
|
+
expect(res.body.user.createdAt).not.toBeUndefined();
|
|
50
|
+
expect(res.body.user.lastUpdated).not.toBeUndefined();
|
|
51
|
+
expect(res.body.user.fullName).toBeUndefined();
|
|
49
52
|
expect(res.body.errors).toBeUndefined();
|
|
50
53
|
expect(res.body.token).not.toBeUndefined();
|
|
51
54
|
expect(res.body.user).not.toBeUndefined();
|
|
52
55
|
});
|
|
53
56
|
});
|
|
54
57
|
|
|
55
|
-
it("POST /
|
|
58
|
+
it("POST /signup returns error on empty data", async () => {
|
|
56
59
|
await request(app)
|
|
57
|
-
.post("/auth/
|
|
60
|
+
.post("/auth/signup")
|
|
58
61
|
.set("Content-Type", "application/json")
|
|
59
62
|
.set("Accept", "application/json")
|
|
60
63
|
.expect(400)
|
|
@@ -64,11 +67,11 @@ describe("/auth", () => {
|
|
|
64
67
|
});
|
|
65
68
|
});
|
|
66
69
|
|
|
67
|
-
it("POST /
|
|
70
|
+
it("POST /signup returns error on short password", async () => {
|
|
68
71
|
await request(app)
|
|
69
|
-
.post("/auth/
|
|
72
|
+
.post("/auth/signup")
|
|
70
73
|
.send({
|
|
71
|
-
username: "
|
|
74
|
+
username: "test" + uuidv4(),
|
|
72
75
|
password: "short",
|
|
73
76
|
})
|
|
74
77
|
.set("Content-Type", "application/json")
|
|
@@ -80,11 +83,11 @@ describe("/auth", () => {
|
|
|
80
83
|
});
|
|
81
84
|
});
|
|
82
85
|
|
|
83
|
-
it("POST /
|
|
86
|
+
it("POST /signup returns error on too long password", async () => {
|
|
84
87
|
await request(app)
|
|
85
|
-
.post("/auth/
|
|
88
|
+
.post("/auth/signup")
|
|
86
89
|
.send({
|
|
87
|
-
username: "
|
|
90
|
+
username: "test" + uuidv4(),
|
|
88
91
|
password:
|
|
89
92
|
"waytolongpasswordtobeusedforthisapplicationyoushouldtrysomethingmuchmuchshorter",
|
|
90
93
|
})
|
|
@@ -97,54 +100,206 @@ describe("/auth", () => {
|
|
|
97
100
|
});
|
|
98
101
|
});
|
|
99
102
|
|
|
100
|
-
it("POST /
|
|
103
|
+
it("POST /signup returns error on already taken username", async () => {
|
|
104
|
+
const data = {
|
|
105
|
+
username: "test" + uuidv4(),
|
|
106
|
+
password: "p@$$w0rd!",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// First call should succeed
|
|
101
110
|
await request(app)
|
|
102
|
-
.post("/auth/
|
|
103
|
-
.send(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
.post("/auth/signup")
|
|
112
|
+
.send(data)
|
|
113
|
+
.set("Content-Type", "application/json")
|
|
114
|
+
.set("Accept", "application/json")
|
|
115
|
+
.expect(200);
|
|
116
|
+
|
|
117
|
+
// Second call will fail
|
|
118
|
+
await request(app)
|
|
119
|
+
.post("/auth/signup")
|
|
120
|
+
.send(data)
|
|
107
121
|
.set("Content-Type", "application/json")
|
|
108
122
|
.set("Accept", "application/json")
|
|
109
123
|
.expect(400)
|
|
110
124
|
.then((res) => {
|
|
111
125
|
expect(res.body).toEqual({
|
|
112
|
-
|
|
126
|
+
success: false,
|
|
127
|
+
message: "A user with this username already exists.",
|
|
113
128
|
});
|
|
114
129
|
});
|
|
115
130
|
});
|
|
116
131
|
|
|
132
|
+
/**
|
|
133
|
+
* @returns Session token
|
|
134
|
+
*/
|
|
135
|
+
async function signupAndLogin(
|
|
136
|
+
username = "test" + uuidv4(),
|
|
137
|
+
password = "p@$$w0rd!"
|
|
138
|
+
) {
|
|
139
|
+
const signup = await request(app)
|
|
140
|
+
.post("/auth/signup")
|
|
141
|
+
.set("Content-Type", "application/json")
|
|
142
|
+
.set("Accept", "application/json")
|
|
143
|
+
.send({
|
|
144
|
+
username,
|
|
145
|
+
password,
|
|
146
|
+
})
|
|
147
|
+
.expect(200);
|
|
148
|
+
|
|
149
|
+
const session = await request(app)
|
|
150
|
+
.post("/auth/login")
|
|
151
|
+
.set("Content-Type", "application/json")
|
|
152
|
+
.set("Accept", "application/json")
|
|
153
|
+
.send({
|
|
154
|
+
username,
|
|
155
|
+
password,
|
|
156
|
+
})
|
|
157
|
+
.expect(200);
|
|
158
|
+
|
|
159
|
+
expect(signup.body.user.id).toEqual(session.body.user.id);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
id: session.body.user.id,
|
|
163
|
+
token: session.body.token,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
117
167
|
it("GET /verify verifies user", async () => {
|
|
118
|
-
const
|
|
168
|
+
const { id } = await signupAndLogin();
|
|
169
|
+
// Verification token should not be part of the response, so load it from the DB
|
|
170
|
+
const user = await dao.getUserById(id);
|
|
119
171
|
|
|
120
172
|
expect(user.verification_token).not.toBe(null);
|
|
121
173
|
expect(user.verified_date).toBeUndefined();
|
|
122
174
|
|
|
123
175
|
// First call will verify the user
|
|
124
|
-
await request(app)
|
|
176
|
+
const verify = await request(app)
|
|
125
177
|
.get("/auth/verify/" + user.verification_token)
|
|
126
178
|
.set("Content-Type", "application/json")
|
|
127
179
|
.set("Accept", "application/json")
|
|
128
|
-
.expect(200)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
180
|
+
.expect(200);
|
|
181
|
+
|
|
182
|
+
expect(verify.body.success).toEqual(true);
|
|
132
183
|
|
|
133
|
-
const refresh = await dao.getUserById(
|
|
184
|
+
const refresh = await dao.getUserById(user.id);
|
|
134
185
|
|
|
135
186
|
expect(refresh.verified_date).not.toBe(null);
|
|
136
187
|
|
|
137
188
|
// Subsequent calls are not successful because the user is already verified
|
|
138
|
-
await request(app)
|
|
189
|
+
const reverify = await request(app)
|
|
139
190
|
.get("/auth/verify/" + user.verification_token)
|
|
140
191
|
.set("Content-Type", "application/json")
|
|
141
192
|
.set("Accept", "application/json")
|
|
142
|
-
.expect(400)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
193
|
+
.expect(400);
|
|
194
|
+
expect(reverify.body).toEqual({
|
|
195
|
+
success: false,
|
|
196
|
+
message: "User already verified.",
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("GET /user retrieves the user", async () => {
|
|
201
|
+
const username = "test" + uuidv4();
|
|
202
|
+
const password = "p@$$w0rd!";
|
|
203
|
+
|
|
204
|
+
const { id, token } = await signupAndLogin(username, password);
|
|
205
|
+
|
|
206
|
+
const user = await request(app)
|
|
207
|
+
.get("/auth/user")
|
|
208
|
+
.set("Content-Type", "application/json")
|
|
209
|
+
.set("Accept", "application/json")
|
|
210
|
+
.set("Authorization", "Bearer " + token);
|
|
211
|
+
|
|
212
|
+
expect(user.status).toEqual(200);
|
|
213
|
+
expect(user.body.id).toEqual(id);
|
|
214
|
+
expect(user.body.username).toEqual(username);
|
|
215
|
+
expect(user.body.createdAt).not.toBeUndefined();
|
|
216
|
+
expect(user.body.lastUpdated).not.toBeUndefined();
|
|
217
|
+
expect(user.body.fullName).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("UPDATE /user updates the user", async () => {
|
|
221
|
+
const { id, token } = await signupAndLogin();
|
|
222
|
+
|
|
223
|
+
const check = await dao.getUserById(id);
|
|
224
|
+
|
|
225
|
+
expect(check.name).toBeFalsy();
|
|
226
|
+
expect(check.email).toBeFalsy();
|
|
227
|
+
|
|
228
|
+
const data = {
|
|
229
|
+
name: "Test User",
|
|
230
|
+
email: "test@test.com",
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const update = await request(app)
|
|
234
|
+
.put("/auth/user")
|
|
235
|
+
.send(data)
|
|
236
|
+
.set("Content-Type", "application/json")
|
|
237
|
+
.set("Accept", "application/json")
|
|
238
|
+
.set("Authorization", "Bearer " + token);
|
|
239
|
+
|
|
240
|
+
expect(update.status).toEqual(200);
|
|
241
|
+
|
|
242
|
+
const user = await dao.getUserById(id);
|
|
243
|
+
|
|
244
|
+
expect(user.name).toEqual(data.name);
|
|
245
|
+
expect(user.email).toEqual(data.email);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("DELETE /user deletes the user", async () => {
|
|
249
|
+
const { id, token } = await signupAndLogin();
|
|
250
|
+
|
|
251
|
+
const del = await request(app)
|
|
252
|
+
.delete("/auth/user")
|
|
253
|
+
.set("Content-Type", "application/json")
|
|
254
|
+
.set("Accept", "application/json")
|
|
255
|
+
.set("Authorization", "Bearer " + token);
|
|
256
|
+
|
|
257
|
+
expect(del.status).toEqual(200);
|
|
258
|
+
|
|
259
|
+
const user = await dao.getUserById(id);
|
|
260
|
+
|
|
261
|
+
expect(user).toBeFalsy();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("POST /password updates the password", async () => {
|
|
265
|
+
const username = "test" + uuidv4();
|
|
266
|
+
const password1 = "FirstP@ssw0rd";
|
|
267
|
+
const password2 = "SecondP@ssw0rd";
|
|
268
|
+
const data = { password: password2, current_password: password1 };
|
|
269
|
+
|
|
270
|
+
const { token } = await signupAndLogin(username, password1);
|
|
271
|
+
|
|
272
|
+
const reset = await request(app)
|
|
273
|
+
.post("/auth/password")
|
|
274
|
+
.send(data)
|
|
275
|
+
.set("Content-Type", "application/json")
|
|
276
|
+
.set("Accept", "application/json")
|
|
277
|
+
.set("Authorization", "Bearer " + token);
|
|
278
|
+
|
|
279
|
+
expect(reset.status).toEqual(200);
|
|
280
|
+
|
|
281
|
+
// Using the old password should fail
|
|
282
|
+
await request(app)
|
|
283
|
+
.post("/auth/login")
|
|
284
|
+
.set("Content-Type", "application/json")
|
|
285
|
+
.set("Accept", "application/json")
|
|
286
|
+
.send({
|
|
287
|
+
username,
|
|
288
|
+
password: password1,
|
|
289
|
+
})
|
|
290
|
+
.expect(401);
|
|
291
|
+
|
|
292
|
+
// Using the new password should succeed
|
|
293
|
+
const attempt2 = await request(app)
|
|
294
|
+
.post("/auth/login")
|
|
295
|
+
.set("Content-Type", "application/json")
|
|
296
|
+
.set("Accept", "application/json")
|
|
297
|
+
.send({
|
|
298
|
+
username,
|
|
299
|
+
password: password2,
|
|
300
|
+
})
|
|
301
|
+
.expect(200);
|
|
302
|
+
|
|
303
|
+
expect(attempt2.body.user.username).toEqual(username);
|
|
149
304
|
});
|
|
150
305
|
});
|