@terreno/api 0.13.2 → 0.14.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/dist/__tests__/versionCheckPlugin.test.js +53 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.asyncHandler.test.d.ts +1 -0
- package/dist/api.asyncHandler.test.js +236 -0
- package/dist/api.d.ts +15 -4
- package/dist/api.errors.test.js +1 -0
- package/dist/api.hooks.test.js +1 -0
- package/dist/api.js +153 -104
- package/dist/api.query.test.js +1 -0
- package/dist/api.test.js +174 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +163 -90
- package/dist/auth.test.js +159 -0
- package/dist/betterAuthApp.test.js +1 -0
- package/dist/betterAuthSetup.d.ts +5 -6
- package/dist/betterAuthSetup.js +17 -14
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +248 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +328 -0
- package/dist/configuration.test.js +1 -0
- package/dist/configurationApp.d.ts +1 -1
- package/dist/configurationApp.js +17 -13
- package/dist/configurationPlugin.test.js +1 -0
- package/dist/consentApp.test.js +1 -0
- package/dist/envConfigurationPlugin.d.ts +2 -0
- package/dist/envConfigurationPlugin.js +173 -0
- package/dist/envConfigurationPlugin.test.d.ts +1 -0
- package/dist/envConfigurationPlugin.test.js +322 -0
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +106 -10
- package/dist/errors.test.js +16 -1
- package/dist/example.js +16 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +53 -2
- package/dist/githubAuth.d.ts +2 -1
- package/dist/githubAuth.js +41 -26
- package/dist/githubAuth.test.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +42 -20
- package/dist/models/versionConfig.d.ts +2 -0
- package/dist/models/versionConfig.js +8 -0
- package/dist/notifiers/googleChatNotifier.js +14 -16
- package/dist/notifiers/googleChatNotifier.test.js +1 -0
- package/dist/notifiers/slackNotifier.js +16 -14
- package/dist/notifiers/slackNotifier.test.js +41 -3
- package/dist/notifiers/zoomNotifier.js +7 -10
- package/dist/notifiers/zoomNotifier.test.js +1 -0
- package/dist/openApi.d.ts +1 -1
- package/dist/openApi.test.js +1 -0
- package/dist/openApiBuilder.d.ts +39 -6
- package/dist/openApiBuilder.js +1 -31
- package/dist/openApiBuilder.test.js +1 -0
- package/dist/openApiValidator.js +1 -0
- package/dist/openApiValidator.test.js +65 -0
- package/dist/permissions.d.ts +4 -4
- package/dist/permissions.js +67 -65
- package/dist/permissions.middleware.test.js +1 -0
- package/dist/permissions.test.js +1 -0
- package/dist/plugins.d.ts +5 -5
- package/dist/plugins.js +18 -9
- package/dist/plugins.test.js +1 -1
- package/dist/populate.d.ts +15 -8
- package/dist/populate.js +23 -24
- package/dist/populate.test.js +1 -0
- package/dist/realtime/changeStreamWatcher.d.ts +73 -0
- package/dist/realtime/changeStreamWatcher.js +720 -0
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime/index.js +27 -0
- package/dist/realtime/queryMatcher.d.ts +14 -0
- package/dist/realtime/queryMatcher.js +250 -0
- package/dist/realtime/queryStore.d.ts +37 -0
- package/dist/realtime/queryStore.js +195 -0
- package/dist/realtime/realtime.test.d.ts +10 -0
- package/dist/realtime/realtime.test.js +2158 -0
- package/dist/realtime/realtimeApp.d.ts +93 -0
- package/dist/realtime/realtimeApp.js +560 -0
- package/dist/realtime/registry.d.ts +40 -0
- package/dist/realtime/registry.js +38 -0
- package/dist/realtime/socketUser.d.ts +10 -0
- package/dist/realtime/socketUser.js +17 -0
- package/dist/realtime/types.d.ts +100 -0
- package/dist/realtime/types.js +2 -0
- package/dist/requestContext.d.ts +37 -0
- package/dist/requestContext.js +344 -0
- package/dist/requestContext.test.d.ts +1 -0
- package/dist/requestContext.test.js +241 -0
- package/dist/terrenoApp.d.ts +8 -0
- package/dist/terrenoApp.js +50 -13
- package/dist/terrenoApp.test.js +194 -21
- package/dist/terrenoPlugin.d.ts +11 -0
- package/dist/tests/bunSetup.js +1 -0
- package/dist/tests.js +1 -1
- package/dist/transformers.d.ts +2 -2
- package/dist/transformers.js +5 -3
- package/dist/transformers.test.js +90 -0
- package/dist/types/consentResponse.d.ts +6 -3
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +18 -12
- package/package.json +4 -2
- package/src/__tests__/versionCheckPlugin.test.ts +37 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.asyncHandler.test.ts +177 -0
- package/src/api.errors.test.ts +1 -0
- package/src/api.hooks.test.ts +1 -0
- package/src/api.query.test.ts +1 -0
- package/src/api.test.ts +132 -0
- package/src/api.ts +199 -84
- package/src/auth.test.ts +160 -0
- package/src/auth.ts +120 -50
- package/src/betterAuthApp.test.ts +1 -0
- package/src/betterAuthSetup.test.ts +1 -0
- package/src/betterAuthSetup.ts +46 -19
- package/src/config.test.ts +255 -0
- package/src/config.ts +206 -0
- package/src/configuration.test.ts +1 -0
- package/src/configurationApp.ts +59 -24
- package/src/configurationPlugin.test.ts +1 -0
- package/src/consentApp.test.ts +1 -0
- package/src/envConfigurationPlugin.test.ts +143 -0
- package/src/envConfigurationPlugin.ts +100 -0
- package/src/errors.test.ts +19 -1
- package/src/errors.ts +94 -20
- package/src/example.ts +46 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +50 -2
- package/src/expressServer.ts +80 -50
- package/src/githubAuth.test.ts +1 -0
- package/src/githubAuth.ts +59 -38
- package/src/index.ts +4 -0
- package/src/logger.ts +47 -17
- package/src/models/versionConfig.ts +13 -2
- package/src/notifiers/googleChatNotifier.test.ts +1 -0
- package/src/notifiers/googleChatNotifier.ts +7 -9
- package/src/notifiers/slackNotifier.test.ts +29 -3
- package/src/notifiers/slackNotifier.ts +9 -7
- package/src/notifiers/zoomNotifier.test.ts +1 -0
- package/src/notifiers/zoomNotifier.ts +8 -11
- package/src/openApi.test.ts +1 -0
- package/src/openApi.ts +4 -4
- package/src/openApiBuilder.test.ts +1 -0
- package/src/openApiBuilder.ts +14 -11
- package/src/openApiValidator.test.ts +59 -0
- package/src/openApiValidator.ts +3 -2
- package/src/permissions.middleware.test.ts +1 -0
- package/src/permissions.test.ts +1 -0
- package/src/permissions.ts +30 -25
- package/src/plugins.test.ts +1 -1
- package/src/plugins.ts +21 -14
- package/src/populate.test.ts +1 -0
- package/src/populate.ts +44 -36
- package/src/realtime/changeStreamWatcher.ts +568 -0
- package/src/realtime/index.ts +34 -0
- package/src/realtime/queryMatcher.ts +179 -0
- package/src/realtime/queryStore.ts +132 -0
- package/src/realtime/realtime.test.ts +1755 -0
- package/src/realtime/realtimeApp.ts +478 -0
- package/src/realtime/registry.ts +64 -0
- package/src/realtime/socketUser.ts +25 -0
- package/src/realtime/types.ts +112 -0
- package/src/requestContext.test.ts +196 -0
- package/src/requestContext.ts +368 -0
- package/src/terrenoApp.test.ts +137 -11
- package/src/terrenoApp.ts +64 -17
- package/src/terrenoPlugin.ts +12 -0
- package/src/tests/bunSetup.ts +1 -0
- package/src/tests.ts +7 -2
- package/src/transformers.test.ts +70 -2
- package/src/transformers.ts +15 -7
- package/src/types/consentResponse.ts +8 -10
- package/src/versionCheckPlugin.ts +15 -7
package/src/auth.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
1
2
|
import {afterEach, beforeEach, describe, expect, it, setSystemTime} from "bun:test";
|
|
2
3
|
import type express from "express";
|
|
3
4
|
import type jwt from "jsonwebtoken";
|
|
@@ -8,13 +9,26 @@ import {modelRouter} from "./api";
|
|
|
8
9
|
import {addAuthRoutes, addMeRoutes, generateTokens, setupAuth} from "./auth";
|
|
9
10
|
import {setupServer} from "./expressServer";
|
|
10
11
|
import {Permissions} from "./permissions";
|
|
12
|
+
import {getCurrentRequestContext} from "./requestContext";
|
|
11
13
|
import {type Food, FoodModel, getBaseServer, setupDb, UserModel} from "./tests";
|
|
12
14
|
import {AdminOwnerTransformer} from "./transformers";
|
|
13
15
|
import {timeout} from "./utils";
|
|
14
16
|
|
|
17
|
+
const decodeTokenPayload = <T extends Record<string, unknown>>(token: string): T => {
|
|
18
|
+
const encodedPayload = token.split(".")[1];
|
|
19
|
+
return JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8")) as T;
|
|
20
|
+
};
|
|
21
|
+
|
|
15
22
|
describe("auth tests", () => {
|
|
16
23
|
let app: express.Application;
|
|
17
24
|
let admin: any;
|
|
25
|
+
let contextEvents: Array<{
|
|
26
|
+
currentSessionId?: string;
|
|
27
|
+
requestId?: string;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
stage: string;
|
|
30
|
+
userId?: string;
|
|
31
|
+
}>;
|
|
18
32
|
let notAdmin: any;
|
|
19
33
|
let agent: TestAgent;
|
|
20
34
|
|
|
@@ -23,6 +37,7 @@ describe("auth tests", () => {
|
|
|
23
37
|
// lockout mechanism needs real time to progress
|
|
24
38
|
setSystemTime();
|
|
25
39
|
[admin, notAdmin] = await setupDb();
|
|
40
|
+
contextEvents = [];
|
|
26
41
|
|
|
27
42
|
await Promise.all([
|
|
28
43
|
FoodModel.create({
|
|
@@ -76,6 +91,65 @@ describe("auth tests", () => {
|
|
|
76
91
|
}),
|
|
77
92
|
})
|
|
78
93
|
);
|
|
94
|
+
router.use(
|
|
95
|
+
"/context-food",
|
|
96
|
+
modelRouter(FoodModel, {
|
|
97
|
+
permissions: {
|
|
98
|
+
create: [Permissions.IsAuthenticated],
|
|
99
|
+
delete: [],
|
|
100
|
+
list: [],
|
|
101
|
+
read: [],
|
|
102
|
+
update: [],
|
|
103
|
+
},
|
|
104
|
+
postCreate: async (_value, req) => {
|
|
105
|
+
contextEvents.push({
|
|
106
|
+
currentSessionId: getCurrentRequestContext()?.sessionId,
|
|
107
|
+
requestId: req.requestId,
|
|
108
|
+
sessionId: req.sessionId,
|
|
109
|
+
stage: "postCreate",
|
|
110
|
+
userId: req.user?.id,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
preCreate: (body, req) => {
|
|
114
|
+
contextEvents.push({
|
|
115
|
+
currentSessionId: getCurrentRequestContext()?.sessionId,
|
|
116
|
+
requestId: req.requestId,
|
|
117
|
+
sessionId: req.sessionId,
|
|
118
|
+
stage: "preCreate",
|
|
119
|
+
userId: req.user?.id,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...(body as Partial<Food>),
|
|
124
|
+
categories: [],
|
|
125
|
+
eatenBy: [req.user?._id],
|
|
126
|
+
expiration: "2026-01-01",
|
|
127
|
+
lastEatenWith: {},
|
|
128
|
+
likesIds: [],
|
|
129
|
+
ownerId: req.user?._id,
|
|
130
|
+
source: {name: "context-test"},
|
|
131
|
+
tags: [],
|
|
132
|
+
} as unknown as Food;
|
|
133
|
+
},
|
|
134
|
+
responseHandler: async (value, method, req) => {
|
|
135
|
+
contextEvents.push({
|
|
136
|
+
currentSessionId: getCurrentRequestContext()?.sessionId,
|
|
137
|
+
requestId: req.requestId,
|
|
138
|
+
sessionId: req.sessionId,
|
|
139
|
+
stage: `responseHandler:${method}`,
|
|
140
|
+
userId: req.user?.id,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
id: String((value as {_id: unknown})._id),
|
|
145
|
+
requestId: req.requestId ?? null,
|
|
146
|
+
sessionContext: getCurrentRequestContext()?.sessionId ?? null,
|
|
147
|
+
sessionId: req.sessionId ?? null,
|
|
148
|
+
userId: req.user?.id ?? null,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
);
|
|
79
153
|
}
|
|
80
154
|
app = setupServer({
|
|
81
155
|
addRoutes,
|
|
@@ -201,6 +275,92 @@ describe("auth tests", () => {
|
|
|
201
275
|
expect(res.body.data.token).toBeDefined();
|
|
202
276
|
});
|
|
203
277
|
|
|
278
|
+
it("passes request and session context through modelRouter hooks", async () => {
|
|
279
|
+
const loginRes = await agent
|
|
280
|
+
.post("/auth/login")
|
|
281
|
+
.send({email: "admin@example.com", password: "securePassword"})
|
|
282
|
+
.expect(200);
|
|
283
|
+
const loginTokenPayload = decodeTokenPayload<{sid?: string}>(loginRes.body.data.token);
|
|
284
|
+
|
|
285
|
+
const createRes = await agent
|
|
286
|
+
.post("/context-food")
|
|
287
|
+
.set("authorization", `Bearer ${loginRes.body.data.token}`)
|
|
288
|
+
.set("X-Request-ID", "model-router-request-1")
|
|
289
|
+
.send({calories: 10, name: "Context Apple"})
|
|
290
|
+
.expect(201);
|
|
291
|
+
|
|
292
|
+
expect(loginTokenPayload.sid).toBeDefined();
|
|
293
|
+
const sessionId = loginTokenPayload.sid;
|
|
294
|
+
if (!sessionId) {
|
|
295
|
+
throw new Error("Expected login token to include a session id");
|
|
296
|
+
}
|
|
297
|
+
expect(createRes.headers["x-request-id"]).toBe("model-router-request-1");
|
|
298
|
+
expect(createRes.headers["x-session-id"]).toBe(sessionId);
|
|
299
|
+
expect(createRes.body.data.requestId).toBe("model-router-request-1");
|
|
300
|
+
expect(createRes.body.data.sessionId).toBe(sessionId);
|
|
301
|
+
expect(createRes.body.data.sessionContext).toBe(sessionId);
|
|
302
|
+
expect(createRes.body.data.userId).toBe(String(admin._id));
|
|
303
|
+
expect(contextEvents).toEqual([
|
|
304
|
+
{
|
|
305
|
+
currentSessionId: sessionId,
|
|
306
|
+
requestId: "model-router-request-1",
|
|
307
|
+
sessionId,
|
|
308
|
+
stage: "preCreate",
|
|
309
|
+
userId: String(admin._id),
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
currentSessionId: sessionId,
|
|
313
|
+
requestId: "model-router-request-1",
|
|
314
|
+
sessionId,
|
|
315
|
+
stage: "postCreate",
|
|
316
|
+
userId: String(admin._id),
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
currentSessionId: sessionId,
|
|
320
|
+
requestId: "model-router-request-1",
|
|
321
|
+
sessionId,
|
|
322
|
+
stage: "responseHandler:create",
|
|
323
|
+
userId: String(admin._id),
|
|
324
|
+
},
|
|
325
|
+
]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("preserves JWT session id across refresh and request context", async () => {
|
|
329
|
+
const loginRes = await agent
|
|
330
|
+
.post("/auth/login")
|
|
331
|
+
.send({email: "admin@example.com", password: "securePassword"})
|
|
332
|
+
.expect(200);
|
|
333
|
+
const loginTokenPayload = decodeTokenPayload<{sid?: string}>(loginRes.body.data.token);
|
|
334
|
+
const loginRefreshPayload = decodeTokenPayload<{sid?: string}>(loginRes.body.data.refreshToken);
|
|
335
|
+
|
|
336
|
+
expect(loginTokenPayload.sid).toBeDefined();
|
|
337
|
+
const loginSessionId = loginTokenPayload.sid;
|
|
338
|
+
if (!loginSessionId) {
|
|
339
|
+
throw new Error("Expected login token to include a session id");
|
|
340
|
+
}
|
|
341
|
+
expect(loginRefreshPayload.sid).toBe(loginSessionId);
|
|
342
|
+
expect(loginRes.headers["x-session-id"]).toBe(loginSessionId);
|
|
343
|
+
|
|
344
|
+
const refreshRes = await agent
|
|
345
|
+
.post("/auth/refresh_token")
|
|
346
|
+
.send({refreshToken: loginRes.body.data.refreshToken})
|
|
347
|
+
.expect(200);
|
|
348
|
+
const refreshedTokenPayload = decodeTokenPayload<{sid?: string}>(refreshRes.body.data.token);
|
|
349
|
+
const refreshedRefreshPayload = decodeTokenPayload<{sid?: string}>(
|
|
350
|
+
refreshRes.body.data.refreshToken
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
expect(refreshedTokenPayload.sid).toBe(loginSessionId);
|
|
354
|
+
expect(refreshedRefreshPayload.sid).toBe(loginSessionId);
|
|
355
|
+
expect(refreshRes.headers["x-session-id"]).toBe(loginSessionId);
|
|
356
|
+
|
|
357
|
+
const foodRes = await agent
|
|
358
|
+
.get("/food")
|
|
359
|
+
.set("authorization", `Bearer ${refreshRes.body.data.token}`)
|
|
360
|
+
.expect(200);
|
|
361
|
+
expect(foodRes.headers["x-session-id"]).toBe(loginSessionId);
|
|
362
|
+
});
|
|
363
|
+
|
|
204
364
|
it("completes token login e2e", async () => {
|
|
205
365
|
const res = await agent
|
|
206
366
|
.post("/auth/login")
|
package/src/auth.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {randomUUID} from "node:crypto";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import jwt, {type JwtPayload} from "jsonwebtoken";
|
|
3
4
|
import type {Model, ObjectId} from "mongoose";
|
|
@@ -11,9 +12,15 @@ import {
|
|
|
11
12
|
} from "passport-jwt";
|
|
12
13
|
import {Strategy as LocalStrategy} from "passport-local";
|
|
13
14
|
|
|
14
|
-
import {APIError, apiErrorMiddleware} from "./errors";
|
|
15
|
+
import {APIError, apiErrorMiddleware, errorMessage} from "./errors";
|
|
15
16
|
import type {AuthOptions} from "./expressServer";
|
|
16
17
|
import {logger} from "./logger";
|
|
18
|
+
import {
|
|
19
|
+
getSessionIdFromJwtPayload,
|
|
20
|
+
type JwtSessionPayload,
|
|
21
|
+
setRequestContext,
|
|
22
|
+
updateRequestContextFromRequest,
|
|
23
|
+
} from "./requestContext";
|
|
17
24
|
|
|
18
25
|
export interface User {
|
|
19
26
|
_id: ObjectId | string;
|
|
@@ -31,14 +38,22 @@ export interface User {
|
|
|
31
38
|
export interface UserModel extends Model<User> {
|
|
32
39
|
createAnonymousUser?: (id?: string) => Promise<User>;
|
|
33
40
|
// Allows additional setup during signup. This will be passed the rest of req.body from the signup
|
|
34
|
-
postCreate?: (body:
|
|
41
|
+
postCreate?: (body: Record<string, unknown>) => Promise<void>;
|
|
35
42
|
|
|
43
|
+
// biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose return types are untyped
|
|
36
44
|
createStrategy(): any;
|
|
45
|
+
// biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose return types are untyped
|
|
37
46
|
serializeUser(): any;
|
|
47
|
+
// biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose return types are untyped
|
|
38
48
|
deserializeUser(): any;
|
|
49
|
+
// biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose return types are untyped
|
|
39
50
|
findByUsername(username: string, findOpts: any): any;
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
export interface GenerateTokensOptions {
|
|
54
|
+
sessionId?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
export function authenticateMiddleware(anonymous = false) {
|
|
43
58
|
const strategies = ["jwt"];
|
|
44
59
|
if (anonymous) {
|
|
@@ -49,7 +64,7 @@ export function authenticateMiddleware(anonymous = false) {
|
|
|
49
64
|
failWithError: true,
|
|
50
65
|
session: false,
|
|
51
66
|
});
|
|
52
|
-
return (req:
|
|
67
|
+
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
53
68
|
if (req.user) {
|
|
54
69
|
return next();
|
|
55
70
|
}
|
|
@@ -61,27 +76,29 @@ export async function signupUser(
|
|
|
61
76
|
userModel: UserModel,
|
|
62
77
|
email: string,
|
|
63
78
|
password: string,
|
|
64
|
-
body?:
|
|
79
|
+
body?: Record<string, unknown>
|
|
65
80
|
) {
|
|
66
81
|
// Strip email and password from the body. They can cause mongoose to throw an error if strict is
|
|
67
82
|
// set.
|
|
68
|
-
const {email: _email, password: _password, ...bodyRest} = body;
|
|
83
|
+
const {email: _email, password: _password, ...bodyRest} = body ?? {};
|
|
69
84
|
|
|
70
85
|
try {
|
|
86
|
+
// biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose's register() is untyped
|
|
71
87
|
const user = await (userModel as any).register({email, ...bodyRest}, password);
|
|
72
88
|
|
|
73
89
|
if (user.postCreate) {
|
|
74
90
|
try {
|
|
75
91
|
await user.postCreate(bodyRest);
|
|
76
|
-
} catch (error:
|
|
92
|
+
} catch (error: unknown) {
|
|
77
93
|
logger.error(`Error in user.postCreate: ${error}`);
|
|
78
94
|
throw error;
|
|
79
95
|
}
|
|
80
96
|
}
|
|
81
97
|
await user.save();
|
|
82
98
|
return user;
|
|
83
|
-
} catch (error:
|
|
84
|
-
|
|
99
|
+
} catch (error: unknown) {
|
|
100
|
+
const message = errorMessage(error);
|
|
101
|
+
throw new APIError({title: message});
|
|
85
102
|
}
|
|
86
103
|
}
|
|
87
104
|
|
|
@@ -102,16 +119,22 @@ export async function signupUser(
|
|
|
102
119
|
* authentication providers) to reuse and customize the same token generation logic.
|
|
103
120
|
* This ensures consistent and secure token issuance across different authentication flows.
|
|
104
121
|
*/
|
|
105
|
-
export const generateTokens = async (
|
|
122
|
+
export const generateTokens = async (
|
|
123
|
+
user: unknown,
|
|
124
|
+
authOptions?: AuthOptions,
|
|
125
|
+
options: GenerateTokensOptions = {}
|
|
126
|
+
) => {
|
|
106
127
|
const tokenSecretOrKey = process.env.TOKEN_SECRET;
|
|
107
128
|
if (!tokenSecretOrKey) {
|
|
108
129
|
throw new Error("TOKEN_SECRET must be set in env.");
|
|
109
130
|
}
|
|
110
|
-
|
|
131
|
+
const tokenUser = user as {_id?: ObjectId | string} | null | undefined;
|
|
132
|
+
if (!tokenUser?._id) {
|
|
111
133
|
logger.warn("No user found for token generation");
|
|
112
134
|
return {refreshToken: null, token: null};
|
|
113
135
|
}
|
|
114
|
-
|
|
136
|
+
const sessionId = options.sessionId ?? randomUUID();
|
|
137
|
+
let payload: Record<string, unknown> = {id: String(tokenUser._id), sid: sessionId};
|
|
115
138
|
if (authOptions?.generateJWTPayload) {
|
|
116
139
|
payload = {...authOptions.generateJWTPayload(user), ...payload};
|
|
117
140
|
}
|
|
@@ -137,7 +160,7 @@ export const generateTokens = async (user: any, authOptions?: AuthOptions) => {
|
|
|
137
160
|
|
|
138
161
|
const token = jwt.sign(payload, tokenSecretOrKey, tokenOptions);
|
|
139
162
|
const refreshTokenSecretOrKey = process.env.REFRESH_TOKEN_SECRET;
|
|
140
|
-
let refreshToken;
|
|
163
|
+
let refreshToken: string | undefined;
|
|
141
164
|
if (refreshTokenSecretOrKey) {
|
|
142
165
|
const refreshTokenOptions: jwt.SignOptions = {
|
|
143
166
|
expiresIn: "30d",
|
|
@@ -159,7 +182,7 @@ export const generateTokens = async (user: any, authOptions?: AuthOptions) => {
|
|
|
159
182
|
} else {
|
|
160
183
|
logger.info("REFRESH_TOKEN_SECRET not set so refresh tokens will not be issued");
|
|
161
184
|
}
|
|
162
|
-
return {refreshToken, token};
|
|
185
|
+
return {refreshToken, sessionId, token};
|
|
163
186
|
};
|
|
164
187
|
|
|
165
188
|
// TODO allow customization
|
|
@@ -215,7 +238,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
|
|
|
215
238
|
passport.use(
|
|
216
239
|
"jwt",
|
|
217
240
|
new JwtStrategy(jwtOpts, async (jwtPayload: JwtPayload, done) => {
|
|
218
|
-
let user;
|
|
241
|
+
let user: User | null = null;
|
|
219
242
|
if (!jwtPayload) {
|
|
220
243
|
return done(null, false);
|
|
221
244
|
}
|
|
@@ -241,7 +264,11 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
|
|
|
241
264
|
|
|
242
265
|
// Adds req.user to the request. This may wind up duplicating requests with passport,
|
|
243
266
|
// but passport doesn't give us req.user early enough.
|
|
244
|
-
async function decodeJWTMiddleware(
|
|
267
|
+
async function decodeJWTMiddleware(
|
|
268
|
+
req: express.Request,
|
|
269
|
+
res: express.Response,
|
|
270
|
+
next: express.NextFunction
|
|
271
|
+
) {
|
|
245
272
|
if (!process.env.TOKEN_SECRET) {
|
|
246
273
|
return next();
|
|
247
274
|
}
|
|
@@ -260,21 +287,34 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
|
|
|
260
287
|
return next();
|
|
261
288
|
}
|
|
262
289
|
|
|
263
|
-
let decoded;
|
|
290
|
+
let decoded: jwt.JwtPayload | undefined;
|
|
264
291
|
|
|
265
292
|
try {
|
|
266
293
|
decoded = jwt.verify(token, process.env.TOKEN_SECRET, {
|
|
267
294
|
issuer: process.env.TOKEN_ISSUER,
|
|
268
295
|
}) as jwt.JwtPayload;
|
|
269
|
-
} catch (error:
|
|
296
|
+
} catch (error: unknown) {
|
|
270
297
|
const userText = req.user?._id ? ` for user ${req.user._id} ` : "";
|
|
271
|
-
const
|
|
298
|
+
const expiredAt =
|
|
299
|
+
error && typeof error === "object" && "expiredAt" in error
|
|
300
|
+
? (error as {expiredAt?: unknown}).expiredAt
|
|
301
|
+
: undefined;
|
|
302
|
+
const message = errorMessage(error);
|
|
303
|
+
const details = `[jwt] Error decoding token${userText}: ${error}, expired at ${expiredAt}, current time: ${Date.now()}`;
|
|
272
304
|
logger.debug(details);
|
|
273
|
-
return res.status(401).json({details, message
|
|
305
|
+
return res.status(401).json({details, message});
|
|
274
306
|
}
|
|
275
|
-
if (decoded
|
|
307
|
+
if (decoded?.id) {
|
|
308
|
+
const sessionId = getSessionIdFromJwtPayload(decoded as JwtSessionPayload);
|
|
309
|
+
req.authTokenPayload = decoded as JwtSessionPayload;
|
|
310
|
+
if (sessionId) {
|
|
311
|
+
req.sessionId = sessionId;
|
|
312
|
+
setRequestContext({sessionId});
|
|
313
|
+
}
|
|
276
314
|
try {
|
|
277
|
-
|
|
315
|
+
const user = await userModel.findById(decoded.id);
|
|
316
|
+
req.user = user as unknown as express.Request["user"];
|
|
317
|
+
updateRequestContextFromRequest(req, res);
|
|
278
318
|
if (req.user?.disabled) {
|
|
279
319
|
logger.warn(`[jwt] User ${req.user.id} is disabled`);
|
|
280
320
|
return res.status(401).json({status: 401, title: "User is disabled"});
|
|
@@ -286,6 +326,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
|
|
|
286
326
|
return next();
|
|
287
327
|
}
|
|
288
328
|
app.use(decodeJWTMiddleware);
|
|
329
|
+
// biome-ignore lint/suspicious/noExplicitAny: express 5 type for urlencoded doesn't match RequestHandler
|
|
289
330
|
app.use(express.urlencoded({extended: false}) as any);
|
|
290
331
|
}
|
|
291
332
|
|
|
@@ -296,23 +337,35 @@ export function addAuthRoutes(
|
|
|
296
337
|
): void {
|
|
297
338
|
const router = express.Router();
|
|
298
339
|
router.post("/login", async (req, res, next) => {
|
|
299
|
-
passport.authenticate(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
340
|
+
passport.authenticate(
|
|
341
|
+
"local",
|
|
342
|
+
{session: false},
|
|
343
|
+
async (
|
|
344
|
+
err: Error | null,
|
|
345
|
+
user: (User & {type?: string}) | false | null,
|
|
346
|
+
info: {message?: string} | undefined
|
|
347
|
+
) => {
|
|
348
|
+
if (err) {
|
|
349
|
+
logger.error(`Error logging in: ${err}`);
|
|
350
|
+
return next(err);
|
|
351
|
+
}
|
|
352
|
+
if (!user) {
|
|
353
|
+
logger.warn(`Invalid login: ${info}`);
|
|
354
|
+
return res.status(401).json({message: info?.message});
|
|
355
|
+
}
|
|
356
|
+
if (process.env.NODE_ENV !== "test") {
|
|
357
|
+
logger.info(`User logged in: ${user._id}, type: ${user.type || "N/A"}`);
|
|
358
|
+
}
|
|
359
|
+
const tokens = await generateTokens(user, authOptions);
|
|
360
|
+
if (tokens.sessionId) {
|
|
361
|
+
setRequestContext({sessionId: tokens.sessionId, userId: String(user._id)});
|
|
362
|
+
res.setHeader("X-Session-ID", tokens.sessionId);
|
|
363
|
+
}
|
|
364
|
+
return res.json({
|
|
365
|
+
data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: user?._id},
|
|
366
|
+
});
|
|
310
367
|
}
|
|
311
|
-
|
|
312
|
-
return res.json({
|
|
313
|
-
data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: user?._id},
|
|
314
|
-
});
|
|
315
|
-
})(req, res, next);
|
|
368
|
+
)(req, res, next);
|
|
316
369
|
});
|
|
317
370
|
|
|
318
371
|
router.post("/refresh_token", async (req, res) => {
|
|
@@ -329,16 +382,25 @@ export function addAuthRoutes(
|
|
|
329
382
|
return res.status(401).json({message: "No REFRESH_TOKEN_SECRET set, cannot refresh token"});
|
|
330
383
|
}
|
|
331
384
|
const refreshTokenSecretOrKey = process.env.REFRESH_TOKEN_SECRET;
|
|
332
|
-
let decoded;
|
|
385
|
+
let decoded: JwtPayload;
|
|
333
386
|
try {
|
|
334
387
|
decoded = jwt.verify(req.body.refreshToken, refreshTokenSecretOrKey) as JwtPayload;
|
|
335
|
-
} catch (error:
|
|
388
|
+
} catch (error: unknown) {
|
|
336
389
|
logger.error(`Error refreshing token for user ${req.user?.id}: ${error}`);
|
|
337
|
-
|
|
390
|
+
const message = errorMessage(error);
|
|
391
|
+
return res.status(401).json({message});
|
|
338
392
|
}
|
|
339
393
|
if (decoded?.id) {
|
|
340
394
|
const user = await userModel.findById(decoded.id);
|
|
341
|
-
const
|
|
395
|
+
const sessionId = getSessionIdFromJwtPayload(decoded as JwtSessionPayload);
|
|
396
|
+
const tokens = await generateTokens(user, authOptions, {sessionId});
|
|
397
|
+
if (tokens.sessionId) {
|
|
398
|
+
setRequestContext({
|
|
399
|
+
sessionId: tokens.sessionId,
|
|
400
|
+
userId: user?._id ? String(user._id) : undefined,
|
|
401
|
+
});
|
|
402
|
+
res.setHeader("X-Session-ID", tokens.sessionId);
|
|
403
|
+
}
|
|
342
404
|
logger.debug(`Refreshed token for ${user?.id}`);
|
|
343
405
|
return res.json({data: {refreshToken: tokens.refreshToken, token: tokens.token}});
|
|
344
406
|
}
|
|
@@ -351,10 +413,17 @@ export function addAuthRoutes(
|
|
|
351
413
|
router.post(
|
|
352
414
|
"/signup",
|
|
353
415
|
passport.authenticate("signup", {failWithError: true, session: false}),
|
|
354
|
-
async (req:
|
|
416
|
+
async (req: express.Request, res: express.Response) => {
|
|
355
417
|
const tokens = await generateTokens(req.user, authOptions);
|
|
418
|
+
if (tokens.sessionId) {
|
|
419
|
+
setRequestContext({
|
|
420
|
+
sessionId: tokens.sessionId,
|
|
421
|
+
userId: req.user?._id ? String(req.user._id) : undefined,
|
|
422
|
+
});
|
|
423
|
+
res.setHeader("X-Session-ID", tokens.sessionId);
|
|
424
|
+
}
|
|
356
425
|
return res.json({
|
|
357
|
-
data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: req.user
|
|
426
|
+
data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: req.user?._id},
|
|
358
427
|
});
|
|
359
428
|
}
|
|
360
429
|
);
|
|
@@ -379,8 +448,8 @@ export function addMeRoutes(
|
|
|
379
448
|
logger.debug("Not user data found for /me");
|
|
380
449
|
return res.sendStatus(404);
|
|
381
450
|
}
|
|
382
|
-
const dataObject = data.toObject()
|
|
383
|
-
|
|
451
|
+
const dataObject = data.toObject() as unknown as Record<string, unknown>;
|
|
452
|
+
dataObject.id = data._id;
|
|
384
453
|
return res.json({data: dataObject});
|
|
385
454
|
});
|
|
386
455
|
|
|
@@ -396,17 +465,18 @@ export function addMeRoutes(
|
|
|
396
465
|
// try {
|
|
397
466
|
// body = transform(req.body, "update", req.user);
|
|
398
467
|
// } catch (e) {
|
|
399
|
-
// return res.status(403).send({message: (e as
|
|
468
|
+
// return res.status(403).send({message: (e as Error).message});
|
|
400
469
|
// }
|
|
401
470
|
try {
|
|
402
471
|
Object.assign(doc, req.body);
|
|
403
472
|
await doc.save();
|
|
404
473
|
|
|
405
|
-
const dataObject = doc.toObject()
|
|
406
|
-
|
|
474
|
+
const dataObject = doc.toObject() as unknown as Record<string, unknown>;
|
|
475
|
+
dataObject.id = doc._id;
|
|
407
476
|
return res.json({data: dataObject});
|
|
408
|
-
} catch (error) {
|
|
409
|
-
|
|
477
|
+
} catch (error: unknown) {
|
|
478
|
+
const message = errorMessage(error);
|
|
479
|
+
return res.status(403).send({message});
|
|
410
480
|
}
|
|
411
481
|
});
|
|
412
482
|
|