@terreno/api 0.0.1
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/LICENSE +202 -0
- package/README.md +170 -0
- package/biome.jsonc +22 -0
- package/bunfig.toml +4 -0
- package/dist/api.d.ts +227 -0
- package/dist/api.js +1024 -0
- package/dist/api.test.d.ts +1 -0
- package/dist/api.test.js +2143 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +512 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +778 -0
- package/dist/errors.d.ts +75 -0
- package/dist/errors.js +216 -0
- package/dist/example.d.ts +1 -0
- package/dist/example.js +118 -0
- package/dist/expressServer.d.ts +35 -0
- package/dist/expressServer.js +436 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +30 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.js +249 -0
- package/dist/middleware.d.ts +10 -0
- package/dist/middleware.js +52 -0
- package/dist/notifiers/googleChatNotifier.d.ts +5 -0
- package/dist/notifiers/googleChatNotifier.js +130 -0
- package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
- package/dist/notifiers/googleChatNotifier.test.js +260 -0
- package/dist/notifiers/index.d.ts +3 -0
- package/dist/notifiers/index.js +19 -0
- package/dist/notifiers/slackNotifier.d.ts +5 -0
- package/dist/notifiers/slackNotifier.js +130 -0
- package/dist/notifiers/slackNotifier.test.d.ts +1 -0
- package/dist/notifiers/slackNotifier.test.js +259 -0
- package/dist/notifiers/zoomNotifier.d.ts +34 -0
- package/dist/notifiers/zoomNotifier.js +181 -0
- package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
- package/dist/notifiers/zoomNotifier.test.js +370 -0
- package/dist/openApi.d.ts +60 -0
- package/dist/openApi.js +441 -0
- package/dist/openApi.test.d.ts +1 -0
- package/dist/openApi.test.js +445 -0
- package/dist/openApiBuilder.d.ts +419 -0
- package/dist/openApiBuilder.js +424 -0
- package/dist/openApiBuilder.test.d.ts +1 -0
- package/dist/openApiBuilder.test.js +509 -0
- package/dist/openApiEtag.d.ts +7 -0
- package/dist/openApiEtag.js +38 -0
- package/dist/permissions.d.ts +26 -0
- package/dist/permissions.js +331 -0
- package/dist/permissions.test.d.ts +1 -0
- package/dist/permissions.test.js +413 -0
- package/dist/plugins.d.ts +67 -0
- package/dist/plugins.js +315 -0
- package/dist/plugins.test.d.ts +1 -0
- package/dist/plugins.test.js +639 -0
- package/dist/populate.d.ts +14 -0
- package/dist/populate.js +315 -0
- package/dist/populate.test.d.ts +1 -0
- package/dist/populate.test.js +133 -0
- package/dist/response.d.ts +0 -0
- package/dist/response.js +1 -0
- package/dist/tests/bunSetup.d.ts +1 -0
- package/dist/tests/bunSetup.js +297 -0
- package/dist/tests/index.d.ts +1 -0
- package/dist/tests/index.js +17 -0
- package/dist/tests.d.ts +99 -0
- package/dist/tests.js +273 -0
- package/dist/transformers.d.ts +25 -0
- package/dist/transformers.js +217 -0
- package/dist/transformers.test.d.ts +1 -0
- package/dist/transformers.test.js +370 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +143 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +14 -0
- package/index.ts +1 -0
- package/package.json +88 -0
- package/src/__snapshots__/openApi.test.ts.snap +4814 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
- package/src/api.test.ts +1661 -0
- package/src/api.ts +1036 -0
- package/src/auth.test.ts +550 -0
- package/src/auth.ts +408 -0
- package/src/errors.ts +225 -0
- package/src/example.ts +99 -0
- package/src/express.d.ts +5 -0
- package/src/expressServer.ts +387 -0
- package/src/index.ts +14 -0
- package/src/logger.ts +190 -0
- package/src/middleware.ts +18 -0
- package/src/notifiers/googleChatNotifier.test.ts +114 -0
- package/src/notifiers/googleChatNotifier.ts +47 -0
- package/src/notifiers/index.ts +3 -0
- package/src/notifiers/slackNotifier.test.ts +113 -0
- package/src/notifiers/slackNotifier.ts +55 -0
- package/src/notifiers/zoomNotifier.test.ts +207 -0
- package/src/notifiers/zoomNotifier.ts +111 -0
- package/src/openApi.test.ts +331 -0
- package/src/openApi.ts +494 -0
- package/src/openApiBuilder.test.ts +442 -0
- package/src/openApiBuilder.ts +636 -0
- package/src/openApiEtag.ts +40 -0
- package/src/permissions.test.ts +219 -0
- package/src/permissions.ts +228 -0
- package/src/plugins.test.ts +390 -0
- package/src/plugins.ts +289 -0
- package/src/populate.test.ts +65 -0
- package/src/populate.ts +258 -0
- package/src/response.ts +0 -0
- package/src/tests/bunSetup.ts +234 -0
- package/src/tests/index.ts +1 -0
- package/src/tests.ts +218 -0
- package/src/transformers.test.ts +202 -0
- package/src/transformers.ts +170 -0
- package/src/utils.test.ts +14 -0
- package/src/utils.ts +47 -0
- package/tsconfig.json +60 -0
- package/types.d.ts +17 -0
package/src/auth.test.ts
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, setSystemTime} from "bun:test";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
import type jwt from "jsonwebtoken";
|
|
4
|
+
import supertest from "supertest";
|
|
5
|
+
import type TestAgent from "supertest/lib/agent";
|
|
6
|
+
|
|
7
|
+
import {modelRouter} from "./api";
|
|
8
|
+
import {addAuthRoutes, addMeRoutes, generateTokens, setupAuth} from "./auth";
|
|
9
|
+
import {setupServer} from "./expressServer";
|
|
10
|
+
import {Permissions} from "./permissions";
|
|
11
|
+
import {type Food, FoodModel, getBaseServer, setupDb, UserModel} from "./tests";
|
|
12
|
+
import {AdminOwnerTransformer} from "./transformers";
|
|
13
|
+
import {timeout} from "./utils";
|
|
14
|
+
|
|
15
|
+
describe("auth tests", () => {
|
|
16
|
+
let app: express.Application;
|
|
17
|
+
let admin: any;
|
|
18
|
+
let notAdmin: any;
|
|
19
|
+
let agent: TestAgent;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
// Reset to real time - don't freeze time here as passport-local-mongoose
|
|
23
|
+
// lockout mechanism needs real time to progress
|
|
24
|
+
setSystemTime();
|
|
25
|
+
[admin, notAdmin] = await setupDb();
|
|
26
|
+
|
|
27
|
+
await Promise.all([
|
|
28
|
+
FoodModel.create({
|
|
29
|
+
calories: 1,
|
|
30
|
+
created: new Date(),
|
|
31
|
+
name: "Spinach",
|
|
32
|
+
ownerId: notAdmin._id,
|
|
33
|
+
}),
|
|
34
|
+
FoodModel.create({
|
|
35
|
+
calories: 100,
|
|
36
|
+
created: Date.now() - 10,
|
|
37
|
+
hidden: true,
|
|
38
|
+
name: "Apple",
|
|
39
|
+
ownerId: admin._id,
|
|
40
|
+
}),
|
|
41
|
+
FoodModel.create({
|
|
42
|
+
calories: 100,
|
|
43
|
+
created: Date.now() - 10,
|
|
44
|
+
name: "Carrots",
|
|
45
|
+
ownerId: admin._id,
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function addRoutes(router: express.Router): void {
|
|
50
|
+
router.use(
|
|
51
|
+
"/food",
|
|
52
|
+
modelRouter(FoodModel, {
|
|
53
|
+
allowAnonymous: true,
|
|
54
|
+
permissions: {
|
|
55
|
+
create: [Permissions.IsAuthenticated],
|
|
56
|
+
delete: [Permissions.IsAuthenticated],
|
|
57
|
+
list: [Permissions.IsAny],
|
|
58
|
+
read: [Permissions.IsAny],
|
|
59
|
+
update: [Permissions.IsAuthenticated],
|
|
60
|
+
},
|
|
61
|
+
queryFilter: (user?: {admin: boolean}) => {
|
|
62
|
+
if (!user?.admin) {
|
|
63
|
+
return {hidden: {$ne: true}};
|
|
64
|
+
}
|
|
65
|
+
return {};
|
|
66
|
+
},
|
|
67
|
+
transformer: AdminOwnerTransformer<Food>({
|
|
68
|
+
adminReadFields: ["name", "calories", "created", "ownerId"],
|
|
69
|
+
adminWriteFields: ["name", "calories", "created", "ownerId"],
|
|
70
|
+
anonReadFields: ["name"],
|
|
71
|
+
anonWriteFields: [],
|
|
72
|
+
authReadFields: ["name", "calories", "created"],
|
|
73
|
+
authWriteFields: ["name", "calories"],
|
|
74
|
+
ownerReadFields: ["name", "calories", "created", "ownerId"],
|
|
75
|
+
ownerWriteFields: ["name", "calories", "created"],
|
|
76
|
+
}),
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
app = setupServer({
|
|
81
|
+
addRoutes,
|
|
82
|
+
skipListen: true,
|
|
83
|
+
userModel: UserModel as any,
|
|
84
|
+
});
|
|
85
|
+
agent = supertest.agent(app);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(async () => {
|
|
89
|
+
setSystemTime();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("completes token signup e2e", async () => {
|
|
93
|
+
let res = await agent
|
|
94
|
+
.post("/auth/signup")
|
|
95
|
+
.send({email: "new@example.com", password: "123"})
|
|
96
|
+
.expect(200);
|
|
97
|
+
let {userId, token, refreshToken} = res.body.data;
|
|
98
|
+
expect(userId).toBeDefined();
|
|
99
|
+
expect(token).toBeDefined();
|
|
100
|
+
expect(refreshToken).toBeDefined();
|
|
101
|
+
|
|
102
|
+
res = await agent
|
|
103
|
+
.post("/auth/login")
|
|
104
|
+
.send({email: "new@example.com", password: "123"})
|
|
105
|
+
.expect(200);
|
|
106
|
+
await agent.set("authorization", `Bearer ${res.body.data.token}`);
|
|
107
|
+
|
|
108
|
+
userId = res.body.data.userId;
|
|
109
|
+
token = res.body.data.token;
|
|
110
|
+
expect(userId).toBeDefined();
|
|
111
|
+
expect(token).toBeDefined();
|
|
112
|
+
expect(refreshToken).toBeDefined();
|
|
113
|
+
|
|
114
|
+
const food = await FoodModel.create({
|
|
115
|
+
calories: 1,
|
|
116
|
+
created: new Date(),
|
|
117
|
+
name: "Peas",
|
|
118
|
+
ownerId: userId,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const meRes = await agent.get("/auth/me").expect(200);
|
|
122
|
+
expect(meRes.body.data._id).toBeDefined();
|
|
123
|
+
expect(meRes.body.data.id).toBeDefined();
|
|
124
|
+
expect(meRes.body.data.hash).toBeUndefined();
|
|
125
|
+
expect(meRes.body.data.email).toBe("new@example.com");
|
|
126
|
+
expect(meRes.body.data.updated).toBeDefined();
|
|
127
|
+
expect(meRes.body.data.created).toBeDefined();
|
|
128
|
+
expect(meRes.body.data.admin).toBe(false);
|
|
129
|
+
|
|
130
|
+
const mePatchRes = await agent
|
|
131
|
+
.patch("/auth/me")
|
|
132
|
+
.send({email: "new2@example.com"})
|
|
133
|
+
.set("authorization", `Bearer ${token}`)
|
|
134
|
+
.expect(200);
|
|
135
|
+
expect(mePatchRes.body.data._id).toBeDefined();
|
|
136
|
+
expect(mePatchRes.body.data.id).toBeDefined();
|
|
137
|
+
expect(mePatchRes.body.data.hash).toBeUndefined();
|
|
138
|
+
expect(mePatchRes.body.data.email).toBe("new2@example.com");
|
|
139
|
+
expect(mePatchRes.body.data.updated).toBeDefined();
|
|
140
|
+
expect(mePatchRes.body.data.created).toBeDefined();
|
|
141
|
+
expect(mePatchRes.body.data.admin).toBe(false);
|
|
142
|
+
|
|
143
|
+
// Use token to see 2 foods + the one we just created
|
|
144
|
+
const getRes = await agent.get("/food").expect(200);
|
|
145
|
+
|
|
146
|
+
expect(getRes.body.data).toHaveLength(3);
|
|
147
|
+
expect(getRes.body.data.find((f: any) => f.name === "Peas")).toBeDefined();
|
|
148
|
+
|
|
149
|
+
const updateRes = await agent
|
|
150
|
+
.patch(`/food/${food._id}`)
|
|
151
|
+
.send({name: "PeasAndCarrots"})
|
|
152
|
+
.expect(200);
|
|
153
|
+
expect(updateRes.body.data.name).toBe("PeasAndCarrots");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("signup with extra data", async () => {
|
|
157
|
+
const res = await agent
|
|
158
|
+
.post("/auth/signup")
|
|
159
|
+
.send({age: 25, email: "new@example.com", password: "123"})
|
|
160
|
+
.expect(200);
|
|
161
|
+
const {userId, token, refreshToken} = res.body.data;
|
|
162
|
+
expect(userId).toBeDefined();
|
|
163
|
+
expect(token).toBeDefined();
|
|
164
|
+
expect(refreshToken).toBeDefined();
|
|
165
|
+
|
|
166
|
+
const user = await UserModel.findOne({email: "new@example.com"});
|
|
167
|
+
expect(user?.age).toBe(25);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("login failure", async () => {
|
|
171
|
+
let res = await agent
|
|
172
|
+
.post("/auth/login")
|
|
173
|
+
.send({email: "admin@example.com", password: "wrong"})
|
|
174
|
+
.expect(401);
|
|
175
|
+
expect(res.body).toEqual({
|
|
176
|
+
message: "Password or username is incorrect",
|
|
177
|
+
});
|
|
178
|
+
res = await agent
|
|
179
|
+
.post("/auth/login")
|
|
180
|
+
.send({email: "nope@example.com", password: "wrong"})
|
|
181
|
+
.expect(401);
|
|
182
|
+
// we don't really want to expose if a given email address has an account in our system or not
|
|
183
|
+
expect(res.body).toEqual({
|
|
184
|
+
message: "Password or username is incorrect",
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("case insensitive email", async () => {
|
|
189
|
+
const res = await agent
|
|
190
|
+
.post("/auth/login")
|
|
191
|
+
.send({email: "ADMIN@example.com", password: "securePassword"})
|
|
192
|
+
.expect(200);
|
|
193
|
+
expect(res.body.data.token).toBeDefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("case insensitive email with emails with symbols", async () => {
|
|
197
|
+
const res = await agent
|
|
198
|
+
.post("/auth/login")
|
|
199
|
+
.send({email: "ADMIN+other@example.com", password: "otherPassword"})
|
|
200
|
+
.expect(200);
|
|
201
|
+
expect(res.body.data.token).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("completes token login e2e", async () => {
|
|
205
|
+
const res = await agent
|
|
206
|
+
.post("/auth/login")
|
|
207
|
+
.send({email: "admin@example.com", password: "securePassword"})
|
|
208
|
+
.expect(200);
|
|
209
|
+
const {userId, token} = res.body.data;
|
|
210
|
+
expect(userId).toBeDefined();
|
|
211
|
+
expect(token).toBeDefined();
|
|
212
|
+
|
|
213
|
+
await agent.set("authorization", `Bearer ${res.body.data.token}`);
|
|
214
|
+
|
|
215
|
+
const meRes = await agent.get("/auth/me").expect(200);
|
|
216
|
+
expect(meRes.body.data._id).toBeDefined();
|
|
217
|
+
expect(meRes.body.data.id).toBeDefined();
|
|
218
|
+
expect(meRes.body.data.hash).toBeUndefined();
|
|
219
|
+
expect(meRes.body.data.email).toBe("admin@example.com");
|
|
220
|
+
expect(meRes.body.data.updated).toBeDefined();
|
|
221
|
+
expect(meRes.body.data.created).toBeDefined();
|
|
222
|
+
expect(meRes.body.data.admin).toBe(true);
|
|
223
|
+
|
|
224
|
+
const mePatchRes = await agent
|
|
225
|
+
.patch("/auth/me")
|
|
226
|
+
.send({email: "admin2@example.com"})
|
|
227
|
+
.expect(200);
|
|
228
|
+
expect(mePatchRes.body.data._id).toBeDefined();
|
|
229
|
+
expect(mePatchRes.body.data.id).toBeDefined();
|
|
230
|
+
expect(mePatchRes.body.data.hash).toBeUndefined();
|
|
231
|
+
expect(mePatchRes.body.data.email).toBe("admin2@example.com");
|
|
232
|
+
expect(mePatchRes.body.data.updated).toBeDefined();
|
|
233
|
+
expect(mePatchRes.body.data.created).toBeDefined();
|
|
234
|
+
expect(mePatchRes.body.data.admin).toBe(true);
|
|
235
|
+
|
|
236
|
+
// Use token to see admin foods
|
|
237
|
+
const getRes = await agent.get("/food").expect(200);
|
|
238
|
+
|
|
239
|
+
expect(getRes.body.data).toHaveLength(3);
|
|
240
|
+
const food = getRes.body.data.find((f: any) => f.name === "Apple");
|
|
241
|
+
expect(food).toBeDefined();
|
|
242
|
+
|
|
243
|
+
const updateRes = await agent
|
|
244
|
+
.patch(`/food/${food.id}`)
|
|
245
|
+
.set("authorization", `Bearer ${token}`)
|
|
246
|
+
.send({name: "Apple Pie"})
|
|
247
|
+
.expect(200);
|
|
248
|
+
expect(updateRes.body.data.name).toBe("Apple Pie");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("login successfully and tokens expire", async () => {
|
|
252
|
+
const res = await agent
|
|
253
|
+
.post("/auth/login")
|
|
254
|
+
.send({email: "admin@example.com", password: "securePassword"})
|
|
255
|
+
.expect(200);
|
|
256
|
+
const {userId, token} = res.body.data;
|
|
257
|
+
expect(userId).toBeDefined();
|
|
258
|
+
expect(token).toBeDefined();
|
|
259
|
+
|
|
260
|
+
await agent.set("authorization", `Bearer ${res.body.data.token}`);
|
|
261
|
+
|
|
262
|
+
await agent.get("/auth/me").expect(200);
|
|
263
|
+
|
|
264
|
+
// Advance time to past token expiration
|
|
265
|
+
setSystemTime(Date.now() + 1000 * 60 * 60 * 24 * 30);
|
|
266
|
+
|
|
267
|
+
await agent.get("/auth/me").expect(401);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("locks out after failed password attempts", async () => {
|
|
271
|
+
let res = await agent
|
|
272
|
+
.post("/auth/login")
|
|
273
|
+
.send({email: "admin@example.com", password: "wrong"})
|
|
274
|
+
.expect(401);
|
|
275
|
+
|
|
276
|
+
expect(res.body).toEqual({
|
|
277
|
+
message: "Password or username is incorrect",
|
|
278
|
+
});
|
|
279
|
+
let user = await UserModel.findById(admin._id);
|
|
280
|
+
expect((user as any)?.attempts).toBe(1);
|
|
281
|
+
res = await agent
|
|
282
|
+
.post("/auth/login")
|
|
283
|
+
.send({email: "admin@example.com", password: "wrong"})
|
|
284
|
+
.expect(401);
|
|
285
|
+
|
|
286
|
+
expect(res.body).toEqual({
|
|
287
|
+
message: "Password or username is incorrect",
|
|
288
|
+
});
|
|
289
|
+
user = await UserModel.findById(admin._id);
|
|
290
|
+
expect((user as any)?.attempts).toBe(2);
|
|
291
|
+
res = await agent
|
|
292
|
+
.post("/auth/login")
|
|
293
|
+
.send({email: "admin@example.com", password: "wrong"})
|
|
294
|
+
.expect(401);
|
|
295
|
+
|
|
296
|
+
expect(res.body).toEqual({
|
|
297
|
+
message: "Account locked due to too many failed login attempts",
|
|
298
|
+
});
|
|
299
|
+
user = await UserModel.findById(admin._id);
|
|
300
|
+
expect((user as any)?.attempts).toBe(3);
|
|
301
|
+
|
|
302
|
+
// Logging in with correct password fails because account is locked
|
|
303
|
+
res = await agent
|
|
304
|
+
.post("/auth/login")
|
|
305
|
+
.send({email: "admin@example.com", password: "securePassword"})
|
|
306
|
+
.expect(401);
|
|
307
|
+
|
|
308
|
+
expect(res.body).toEqual({
|
|
309
|
+
message: "Account locked due to too many failed login attempts",
|
|
310
|
+
});
|
|
311
|
+
user = await UserModel.findById(admin._id);
|
|
312
|
+
// Not incremented
|
|
313
|
+
expect((user as any)?.attempts).toBe(3);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("refresh token allows refresh of auth token", async () => {
|
|
317
|
+
// initial login
|
|
318
|
+
const initialLoginRes = await agent
|
|
319
|
+
.post("/auth/login")
|
|
320
|
+
.send({email: "ADMIN@example.com", password: "securePassword"})
|
|
321
|
+
.expect(200);
|
|
322
|
+
expect(initialLoginRes.body.data.token).toBeDefined();
|
|
323
|
+
expect(initialLoginRes.body.data.refreshToken).toBeDefined();
|
|
324
|
+
const initialToken = initialLoginRes.body.data.token;
|
|
325
|
+
await agent.set("authorization", `Bearer ${initialToken}`);
|
|
326
|
+
|
|
327
|
+
// get new auth token from refresh token
|
|
328
|
+
const refreshRes = await agent
|
|
329
|
+
.post("/auth/refresh_token")
|
|
330
|
+
.send({refreshToken: initialLoginRes.body.data.refreshToken})
|
|
331
|
+
.expect(200);
|
|
332
|
+
expect(refreshRes.body.data.token).toBeDefined();
|
|
333
|
+
expect(refreshRes.body.data.refreshToken).toBeDefined();
|
|
334
|
+
const newToken = refreshRes.body.data.token;
|
|
335
|
+
// note that new token will most likely be the same as the old token because
|
|
336
|
+
// an HMAC signature will always be the same for a header + payload combination that is equal.
|
|
337
|
+
|
|
338
|
+
// make sure new token works
|
|
339
|
+
await agent.set("authorization", `Bearer ${newToken}`);
|
|
340
|
+
const meRes = await agent.get("/auth/me").expect(200);
|
|
341
|
+
expect(meRes.body.data._id).toBeDefined();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("disabled user fails", async () => {
|
|
345
|
+
// initial login
|
|
346
|
+
const initialLoginRes = await agent
|
|
347
|
+
.post("/auth/login")
|
|
348
|
+
.send({email: "ADMIN@example.com", password: "securePassword"})
|
|
349
|
+
.expect(200);
|
|
350
|
+
expect(initialLoginRes.body.data.token).toBeDefined();
|
|
351
|
+
expect(initialLoginRes.body.data.refreshToken).toBeDefined();
|
|
352
|
+
const initialToken = initialLoginRes.body.data.token;
|
|
353
|
+
await agent.set("authorization", `Bearer ${initialToken}`);
|
|
354
|
+
const meRes = await agent.get("/auth/me").expect(200);
|
|
355
|
+
expect(meRes.body.data._id).toBeDefined();
|
|
356
|
+
|
|
357
|
+
admin.disabled = true;
|
|
358
|
+
await admin.save();
|
|
359
|
+
|
|
360
|
+
const failRes = await agent.get("/auth/me").expect(401);
|
|
361
|
+
expect(failRes.body).toEqual({status: 401, title: "User is disabled"});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("signup user with email that is already registered", async () => {
|
|
365
|
+
await agent
|
|
366
|
+
.post("/auth/signup")
|
|
367
|
+
.send({age: 25, email: "new@example.com", password: "123"})
|
|
368
|
+
.expect(200);
|
|
369
|
+
|
|
370
|
+
const res2 = await agent
|
|
371
|
+
.post("/auth/signup")
|
|
372
|
+
.send({age: 31, email: "new@example.com", password: "456"})
|
|
373
|
+
.expect(500);
|
|
374
|
+
|
|
375
|
+
await timeout(1000);
|
|
376
|
+
expect(res2.body.title).toBe("A user with the given username is already registered");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("custom auth options", () => {
|
|
381
|
+
let app: express.Application;
|
|
382
|
+
let admin: any;
|
|
383
|
+
let notAdmin: any;
|
|
384
|
+
|
|
385
|
+
beforeEach(async () => {
|
|
386
|
+
// Reset to real time - don't freeze time here as passport-local-mongoose
|
|
387
|
+
// lockout mechanism needs real time to progress
|
|
388
|
+
setSystemTime();
|
|
389
|
+
[admin, notAdmin] = await setupDb();
|
|
390
|
+
|
|
391
|
+
await Promise.all([
|
|
392
|
+
FoodModel.create({
|
|
393
|
+
calories: 1,
|
|
394
|
+
created: new Date(),
|
|
395
|
+
name: "Spinach",
|
|
396
|
+
ownerId: notAdmin._id,
|
|
397
|
+
}),
|
|
398
|
+
FoodModel.create({
|
|
399
|
+
calories: 100,
|
|
400
|
+
created: Date.now() - 10,
|
|
401
|
+
hidden: true,
|
|
402
|
+
name: "Apple",
|
|
403
|
+
ownerId: admin._id,
|
|
404
|
+
}),
|
|
405
|
+
FoodModel.create({
|
|
406
|
+
calories: 100,
|
|
407
|
+
created: Date.now() - 10,
|
|
408
|
+
name: "Carrots",
|
|
409
|
+
ownerId: admin._id,
|
|
410
|
+
}),
|
|
411
|
+
]);
|
|
412
|
+
app = getBaseServer();
|
|
413
|
+
addAuthRoutes(app, UserModel as any, {
|
|
414
|
+
// custom refresh token logic based on admin or non admin
|
|
415
|
+
generateTokenExpiration: (user?: {admin: boolean}) => {
|
|
416
|
+
if (user?.admin) {
|
|
417
|
+
return "30d";
|
|
418
|
+
}
|
|
419
|
+
return "365d";
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
setupAuth(app, UserModel as any);
|
|
423
|
+
addMeRoutes(app, UserModel as any);
|
|
424
|
+
app.use(
|
|
425
|
+
"/food",
|
|
426
|
+
modelRouter(FoodModel, {
|
|
427
|
+
allowAnonymous: true,
|
|
428
|
+
permissions: {
|
|
429
|
+
create: [Permissions.IsAuthenticated],
|
|
430
|
+
delete: [Permissions.IsAuthenticated],
|
|
431
|
+
list: [Permissions.IsAny],
|
|
432
|
+
read: [Permissions.IsAny],
|
|
433
|
+
update: [Permissions.IsAuthenticated],
|
|
434
|
+
},
|
|
435
|
+
queryFilter: (user?: {admin: boolean}) => {
|
|
436
|
+
if (!user?.admin) {
|
|
437
|
+
return {hidden: {$ne: true}};
|
|
438
|
+
}
|
|
439
|
+
return {};
|
|
440
|
+
},
|
|
441
|
+
transformer: AdminOwnerTransformer<Food>({
|
|
442
|
+
adminReadFields: ["name", "calories", "created", "ownerId"],
|
|
443
|
+
adminWriteFields: ["name", "calories", "created", "ownerId"],
|
|
444
|
+
anonReadFields: ["name"],
|
|
445
|
+
anonWriteFields: [],
|
|
446
|
+
authReadFields: ["name", "calories", "created"],
|
|
447
|
+
authWriteFields: ["name", "calories"],
|
|
448
|
+
ownerReadFields: ["name", "calories", "created", "ownerId"],
|
|
449
|
+
ownerWriteFields: ["name", "calories", "created"],
|
|
450
|
+
}),
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
afterEach(async () => {
|
|
456
|
+
setSystemTime();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("login successfully and tokens expire with custom token options", async () => {
|
|
460
|
+
// login admin and set token
|
|
461
|
+
const adminAgent = supertest.agent(app);
|
|
462
|
+
const res = await adminAgent
|
|
463
|
+
.post("/auth/login")
|
|
464
|
+
.send({email: "admin@example.com", password: "securePassword"})
|
|
465
|
+
.expect(200);
|
|
466
|
+
|
|
467
|
+
expect(res.body.data.userId).toBeDefined();
|
|
468
|
+
expect(res.body.data.token).toBeDefined();
|
|
469
|
+
|
|
470
|
+
await adminAgent.set("authorization", `Bearer ${res.body.data.token}`);
|
|
471
|
+
|
|
472
|
+
// login non-admin and set token
|
|
473
|
+
const notAdminAgent = supertest.agent(app);
|
|
474
|
+
const res2 = await notAdminAgent
|
|
475
|
+
.post("/auth/login")
|
|
476
|
+
.send({email: "notadmin@example.com", password: "password"})
|
|
477
|
+
.expect(200);
|
|
478
|
+
|
|
479
|
+
expect(res2.body.data.userId).toBeDefined();
|
|
480
|
+
expect(res2.body.data.token).toBeDefined();
|
|
481
|
+
|
|
482
|
+
await notAdminAgent.set("authorization", `Bearer ${res2.body.data.token}`);
|
|
483
|
+
|
|
484
|
+
// and check that tokens are working for both users
|
|
485
|
+
await adminAgent.get("/auth/me").expect(200);
|
|
486
|
+
await notAdminAgent.get("/auth/me").expect(200);
|
|
487
|
+
|
|
488
|
+
// Advance time by 30 days check that admin can no longer access with old token,
|
|
489
|
+
// and non-admin can due to custom times set as auth options
|
|
490
|
+
setSystemTime(Date.now() + 1000 * 60 * 60 * 24 * 30);
|
|
491
|
+
await adminAgent.get("/auth/me").expect(401);
|
|
492
|
+
await notAdminAgent.get("/auth/me").expect(200);
|
|
493
|
+
|
|
494
|
+
// Advance time by an additional 335 days to pass the 365 day expiration for non-admin
|
|
495
|
+
setSystemTime(Date.now() + 1000 * 60 * 60 * 24 * 365);
|
|
496
|
+
|
|
497
|
+
// ensure non-admin can no longer access
|
|
498
|
+
await notAdminAgent.get("/auth/me").expect(401);
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe("generateTokens", () => {
|
|
503
|
+
const OLD_ENV = process.env;
|
|
504
|
+
|
|
505
|
+
beforeEach(() => {
|
|
506
|
+
process.env = {...OLD_ENV};
|
|
507
|
+
process.env.TOKEN_SECRET = "secret";
|
|
508
|
+
process.env.REFRESH_TOKEN_SECRET = "refresh_secret";
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
afterEach(() => {
|
|
512
|
+
process.env = OLD_ENV;
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("generates a token and refresh token for a valid user", async () => {
|
|
516
|
+
const user = {_id: "12345"};
|
|
517
|
+
const {token, refreshToken} = await generateTokens(user);
|
|
518
|
+
|
|
519
|
+
expect(token).toBeDefined();
|
|
520
|
+
expect(refreshToken).toBeDefined();
|
|
521
|
+
|
|
522
|
+
// Verify token structure
|
|
523
|
+
const tokenParts = token?.split(".");
|
|
524
|
+
expect(tokenParts?.length).toBe(3);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("throws an error if TOKEN_SECRET is missing", async () => {
|
|
528
|
+
process.env.TOKEN_SECRET = undefined;
|
|
529
|
+
const user = {_id: "12345"};
|
|
530
|
+
|
|
531
|
+
await expect(generateTokens(user)).rejects.toThrow("TOKEN_SECRET must be set in env.");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("returns null tokens if user is missing", async () => {
|
|
535
|
+
const result = await generateTokens(undefined);
|
|
536
|
+
expect(result).toEqual({refreshToken: null, token: null});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("respects custom expiration from authOptions", async () => {
|
|
540
|
+
const user = {_id: "12345"};
|
|
541
|
+
const authOptions = {
|
|
542
|
+
generateRefreshTokenExpiration: () => "7d" as jwt.SignOptions["expiresIn"],
|
|
543
|
+
generateTokenExpiration: () => "1h" as jwt.SignOptions["expiresIn"],
|
|
544
|
+
};
|
|
545
|
+
const {token, refreshToken} = await generateTokens(user, authOptions);
|
|
546
|
+
|
|
547
|
+
expect(token).toBeDefined();
|
|
548
|
+
expect(refreshToken).toBeDefined();
|
|
549
|
+
});
|
|
550
|
+
});
|