@oneuptime/common 8.0.5574 → 8.0.5576
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/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/StatusPagePrivateUserSession.ts +413 -0
- package/Models/DatabaseModels/UserSession.ts +318 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1762890441920-MigrationName.ts +91 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/StatusPagePrivateUserSessionService.ts +368 -0
- package/Server/Services/UserSessionService.ts +350 -0
- package/Server/Utils/Cookie.ts +122 -5
- package/Server/Utils/Express.ts +100 -0
- package/Server/Utils/JsonWebToken.ts +8 -1
- package/Types/CookieName.ts +1 -0
- package/Types/JsonWebTokenData.ts +1 -0
- package/Types/Text.ts +15 -0
- package/UI/Utils/API/API.ts +39 -1
- package/Utils/API.ts +74 -5
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUserSession.js +456 -0
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUserSession.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserSession.js +356 -0
- package/build/dist/Models/DatabaseModels/UserSession.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1762890441920-MigrationName.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1762890441920-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +214 -0
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -0
- package/build/dist/Server/Services/UserSessionService.js +202 -0
- package/build/dist/Server/Services/UserSessionService.js.map +1 -0
- package/build/dist/Server/Utils/Cookie.js +74 -7
- package/build/dist/Server/Utils/Cookie.js.map +1 -1
- package/build/dist/Server/Utils/Express.js +62 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/JsonWebToken.js +8 -2
- package/build/dist/Server/Utils/JsonWebToken.js.map +1 -1
- package/build/dist/Types/CookieName.js +1 -0
- package/build/dist/Types/CookieName.js.map +1 -1
- package/build/dist/Types/Text.js +9 -0
- package/build/dist/Types/Text.js.map +1 -1
- package/build/dist/UI/Utils/API/API.js +27 -0
- package/build/dist/UI/Utils/API/API.js.map +1 -1
- package/build/dist/Utils/API.js +36 -6
- package/build/dist/Utils/API.js.map +1 -1
- package/package.json +146 -146
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import DatabaseService from "./DatabaseService";
|
|
2
|
+
import Model from "../../Models/DatabaseModels/StatusPagePrivateUserSession";
|
|
3
|
+
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import { JSONObject } from "../../Types/JSON";
|
|
5
|
+
import HashedString from "../../Types/HashedString";
|
|
6
|
+
import { EncryptionSecret } from "../EnvironmentConfig";
|
|
7
|
+
import OneUptimeDate from "../../Types/Date";
|
|
8
|
+
import Text from "../../Types/Text";
|
|
9
|
+
import logger from "../Utils/Logger";
|
|
10
|
+
import Exception from "../../Types/Exception/Exception";
|
|
11
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
12
|
+
|
|
13
|
+
export interface SessionMetadata {
|
|
14
|
+
session: Model;
|
|
15
|
+
refreshToken: string;
|
|
16
|
+
refreshTokenExpiresAt: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CreateSessionOptions {
|
|
20
|
+
projectId: ObjectID;
|
|
21
|
+
statusPageId: ObjectID;
|
|
22
|
+
statusPagePrivateUserId: ObjectID;
|
|
23
|
+
refreshToken?: string | undefined;
|
|
24
|
+
refreshTokenExpiresAt?: Date | undefined;
|
|
25
|
+
ipAddress?: string | undefined;
|
|
26
|
+
userAgent?: string | undefined;
|
|
27
|
+
deviceName?: string | undefined;
|
|
28
|
+
deviceType?: string | undefined;
|
|
29
|
+
deviceOS?: string | undefined;
|
|
30
|
+
deviceBrowser?: string | undefined;
|
|
31
|
+
additionalInfo?: JSONObject | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RenewSessionOptions {
|
|
35
|
+
session: Model;
|
|
36
|
+
refreshTokenExpiresAt?: Date | undefined;
|
|
37
|
+
ipAddress?: string | undefined;
|
|
38
|
+
userAgent?: string | undefined;
|
|
39
|
+
deviceName?: string | undefined;
|
|
40
|
+
deviceType?: string | undefined;
|
|
41
|
+
deviceOS?: string | undefined;
|
|
42
|
+
deviceBrowser?: string | undefined;
|
|
43
|
+
additionalInfo?: JSONObject | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TouchSessionOptions {
|
|
47
|
+
ipAddress?: string | undefined;
|
|
48
|
+
userAgent?: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RevokeSessionOptions {
|
|
52
|
+
reason?: string | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class Service extends DatabaseService<Model> {
|
|
56
|
+
private static readonly DEFAULT_REFRESH_TOKEN_TTL_DAYS: number = 30;
|
|
57
|
+
private static readonly SHORT_TEXT_LIMIT: number = 100;
|
|
58
|
+
|
|
59
|
+
public constructor() {
|
|
60
|
+
super(Model);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async createSession(
|
|
64
|
+
options: CreateSessionOptions,
|
|
65
|
+
): Promise<SessionMetadata> {
|
|
66
|
+
const refreshToken: string =
|
|
67
|
+
options.refreshToken || Service.generateRefreshToken();
|
|
68
|
+
const refreshTokenExpiresAt: Date =
|
|
69
|
+
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
|
70
|
+
|
|
71
|
+
const session: Model = this.buildSessionModel(options, {
|
|
72
|
+
refreshToken,
|
|
73
|
+
refreshTokenExpiresAt,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const createdSession: Model = await this.create({
|
|
78
|
+
data: session,
|
|
79
|
+
props: {
|
|
80
|
+
isRoot: true,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
session: createdSession,
|
|
86
|
+
refreshToken,
|
|
87
|
+
refreshTokenExpiresAt,
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw error as Exception;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public async findActiveSessionByRefreshToken(
|
|
95
|
+
refreshToken: string,
|
|
96
|
+
): Promise<Model | null> {
|
|
97
|
+
const hashedValue: string = await HashedString.hashValue(
|
|
98
|
+
refreshToken,
|
|
99
|
+
EncryptionSecret,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const session: Model | null = await this.findOneBy({
|
|
103
|
+
query: {
|
|
104
|
+
refreshToken: new HashedString(hashedValue, true),
|
|
105
|
+
isRevoked: false,
|
|
106
|
+
},
|
|
107
|
+
select: {
|
|
108
|
+
_id: true,
|
|
109
|
+
projectId: true,
|
|
110
|
+
statusPageId: true,
|
|
111
|
+
statusPagePrivateUserId: true,
|
|
112
|
+
refreshTokenExpiresAt: true,
|
|
113
|
+
lastActiveAt: true,
|
|
114
|
+
additionalInfo: true,
|
|
115
|
+
deviceName: true,
|
|
116
|
+
deviceType: true,
|
|
117
|
+
deviceOS: true,
|
|
118
|
+
deviceBrowser: true,
|
|
119
|
+
ipAddress: true,
|
|
120
|
+
userAgent: true,
|
|
121
|
+
isRevoked: true,
|
|
122
|
+
},
|
|
123
|
+
props: {
|
|
124
|
+
isRoot: true,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!session) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
!session.refreshTokenExpiresAt ||
|
|
134
|
+
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
|
|
135
|
+
) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return session;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async renewSessionWithNewRefreshToken(
|
|
143
|
+
options: RenewSessionOptions,
|
|
144
|
+
): Promise<SessionMetadata> {
|
|
145
|
+
const refreshToken: string = Service.generateRefreshToken();
|
|
146
|
+
const refreshTokenExpiresAt: Date =
|
|
147
|
+
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
|
148
|
+
|
|
149
|
+
const updatePayload: Partial<Model> = {
|
|
150
|
+
refreshToken: HashedString.fromString(refreshToken),
|
|
151
|
+
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
|
152
|
+
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
|
153
|
+
isRevoked: false,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const ipAddress: string | undefined = Text.truncate(
|
|
157
|
+
options.ipAddress,
|
|
158
|
+
Service.SHORT_TEXT_LIMIT,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (ipAddress) {
|
|
162
|
+
updatePayload.ipAddress = ipAddress;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (options.userAgent) {
|
|
166
|
+
updatePayload.userAgent = options.userAgent;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const deviceName: string | undefined = Text.truncate(
|
|
170
|
+
options.deviceName,
|
|
171
|
+
Service.SHORT_TEXT_LIMIT,
|
|
172
|
+
);
|
|
173
|
+
if (deviceName) {
|
|
174
|
+
updatePayload.deviceName = deviceName;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const deviceType: string | undefined = Text.truncate(
|
|
178
|
+
options.deviceType,
|
|
179
|
+
Service.SHORT_TEXT_LIMIT,
|
|
180
|
+
);
|
|
181
|
+
if (deviceType) {
|
|
182
|
+
updatePayload.deviceType = deviceType;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const deviceOS: string | undefined = Text.truncate(
|
|
186
|
+
options.deviceOS,
|
|
187
|
+
Service.SHORT_TEXT_LIMIT,
|
|
188
|
+
);
|
|
189
|
+
if (deviceOS) {
|
|
190
|
+
updatePayload.deviceOS = deviceOS;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const deviceBrowser: string | undefined = Text.truncate(
|
|
194
|
+
options.deviceBrowser,
|
|
195
|
+
Service.SHORT_TEXT_LIMIT,
|
|
196
|
+
);
|
|
197
|
+
if (deviceBrowser) {
|
|
198
|
+
updatePayload.deviceBrowser = deviceBrowser;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (options.additionalInfo || options.session.additionalInfo) {
|
|
202
|
+
updatePayload.additionalInfo = {
|
|
203
|
+
...(options.session.additionalInfo || {}),
|
|
204
|
+
...(options.additionalInfo || {}),
|
|
205
|
+
} as JSONObject;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const updatedSession: Model | null = await this.updateOneByIdAndFetch({
|
|
209
|
+
id: options.session.id!,
|
|
210
|
+
data: updatePayload as any,
|
|
211
|
+
props: {
|
|
212
|
+
isRoot: true,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!updatedSession) {
|
|
217
|
+
throw new BadDataException("Unable to renew status page user session");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
session: updatedSession,
|
|
222
|
+
refreshToken,
|
|
223
|
+
refreshTokenExpiresAt,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public async touchSession(
|
|
228
|
+
sessionId: ObjectID,
|
|
229
|
+
options: TouchSessionOptions,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
const updatePayload: Partial<Model> = {
|
|
232
|
+
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const ipAddress: string | undefined = Text.truncate(
|
|
236
|
+
options.ipAddress,
|
|
237
|
+
Service.SHORT_TEXT_LIMIT,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (ipAddress) {
|
|
241
|
+
updatePayload.ipAddress = ipAddress;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (options.userAgent) {
|
|
245
|
+
updatePayload.userAgent = options.userAgent;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await this.updateOneById({
|
|
250
|
+
id: sessionId,
|
|
251
|
+
data: updatePayload as any,
|
|
252
|
+
props: {
|
|
253
|
+
isRoot: true,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
logger.warn(
|
|
258
|
+
`Failed to update status page session activity for session ${sessionId.toString()}: ${(err as Error).message}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
public async revokeSessionById(
|
|
264
|
+
sessionId: ObjectID,
|
|
265
|
+
options?: RevokeSessionOptions,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
await this.updateOneById({
|
|
268
|
+
id: sessionId,
|
|
269
|
+
data: {
|
|
270
|
+
isRevoked: true,
|
|
271
|
+
revokedAt: OneUptimeDate.getCurrentDate(),
|
|
272
|
+
revokedReason: options?.reason ?? null,
|
|
273
|
+
},
|
|
274
|
+
props: {
|
|
275
|
+
isRoot: true,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public async revokeSessionByRefreshToken(
|
|
281
|
+
refreshToken: string,
|
|
282
|
+
options?: RevokeSessionOptions,
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
const session: Model | null =
|
|
285
|
+
await this.findActiveSessionByRefreshToken(refreshToken);
|
|
286
|
+
|
|
287
|
+
if (!session || !session.id) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await this.revokeSessionById(session.id, options);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private buildSessionModel(
|
|
295
|
+
options: CreateSessionOptions,
|
|
296
|
+
tokenMeta: { refreshToken: string; refreshTokenExpiresAt: Date },
|
|
297
|
+
): Model {
|
|
298
|
+
const session: Model = new Model();
|
|
299
|
+
session.projectId = options.projectId;
|
|
300
|
+
session.statusPageId = options.statusPageId;
|
|
301
|
+
session.statusPagePrivateUserId = options.statusPagePrivateUserId;
|
|
302
|
+
session.refreshToken = HashedString.fromString(tokenMeta.refreshToken);
|
|
303
|
+
session.refreshTokenExpiresAt = tokenMeta.refreshTokenExpiresAt;
|
|
304
|
+
session.lastActiveAt = OneUptimeDate.getCurrentDate();
|
|
305
|
+
|
|
306
|
+
if (options.userAgent) {
|
|
307
|
+
session.userAgent = options.userAgent;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const deviceName: string | undefined = Text.truncate(
|
|
311
|
+
options.deviceName,
|
|
312
|
+
Service.SHORT_TEXT_LIMIT,
|
|
313
|
+
);
|
|
314
|
+
if (deviceName) {
|
|
315
|
+
session.deviceName = deviceName;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const deviceType: string | undefined = Text.truncate(
|
|
319
|
+
options.deviceType,
|
|
320
|
+
Service.SHORT_TEXT_LIMIT,
|
|
321
|
+
);
|
|
322
|
+
if (deviceType) {
|
|
323
|
+
session.deviceType = deviceType;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const deviceOS: string | undefined = Text.truncate(
|
|
327
|
+
options.deviceOS,
|
|
328
|
+
Service.SHORT_TEXT_LIMIT,
|
|
329
|
+
);
|
|
330
|
+
if (deviceOS) {
|
|
331
|
+
session.deviceOS = deviceOS;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const deviceBrowser: string | undefined = Text.truncate(
|
|
335
|
+
options.deviceBrowser,
|
|
336
|
+
Service.SHORT_TEXT_LIMIT,
|
|
337
|
+
);
|
|
338
|
+
if (deviceBrowser) {
|
|
339
|
+
session.deviceBrowser = deviceBrowser;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const ipAddress: string | undefined = Text.truncate(
|
|
343
|
+
options.ipAddress,
|
|
344
|
+
Service.SHORT_TEXT_LIMIT,
|
|
345
|
+
);
|
|
346
|
+
if (ipAddress) {
|
|
347
|
+
session.ipAddress = ipAddress;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
session.additionalInfo = {
|
|
351
|
+
...(options.additionalInfo || {}),
|
|
352
|
+
} as JSONObject;
|
|
353
|
+
|
|
354
|
+
return session;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private static generateRefreshToken(): string {
|
|
358
|
+
return ObjectID.generate().toString();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private static getRefreshTokenExpiry(): Date {
|
|
362
|
+
return OneUptimeDate.getSomeDaysAfter(
|
|
363
|
+
Service.DEFAULT_REFRESH_TOKEN_TTL_DAYS,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default new Service();
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import DatabaseService from "./DatabaseService";
|
|
2
|
+
import Model from "../../Models/DatabaseModels/UserSession";
|
|
3
|
+
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import { JSONObject } from "../../Types/JSON";
|
|
5
|
+
import HashedString from "../../Types/HashedString";
|
|
6
|
+
import { EncryptionSecret } from "../EnvironmentConfig";
|
|
7
|
+
import OneUptimeDate from "../../Types/Date";
|
|
8
|
+
import Text from "../../Types/Text";
|
|
9
|
+
import logger from "../Utils/Logger";
|
|
10
|
+
import Exception from "../../Types/Exception/Exception";
|
|
11
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
12
|
+
|
|
13
|
+
export interface SessionMetadata {
|
|
14
|
+
session: Model;
|
|
15
|
+
refreshToken: string;
|
|
16
|
+
refreshTokenExpiresAt: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CreateSessionOptions {
|
|
20
|
+
userId: ObjectID;
|
|
21
|
+
isGlobalLogin: boolean;
|
|
22
|
+
refreshToken?: string | undefined;
|
|
23
|
+
refreshTokenExpiresAt?: Date | undefined;
|
|
24
|
+
ipAddress?: string | undefined;
|
|
25
|
+
userAgent?: string | undefined;
|
|
26
|
+
deviceName?: string | undefined;
|
|
27
|
+
deviceType?: string | undefined;
|
|
28
|
+
deviceOS?: string | undefined;
|
|
29
|
+
deviceBrowser?: string | undefined;
|
|
30
|
+
additionalInfo?: JSONObject | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RenewSessionOptions {
|
|
34
|
+
session: Model;
|
|
35
|
+
refreshTokenExpiresAt?: Date | undefined;
|
|
36
|
+
ipAddress?: string | undefined;
|
|
37
|
+
userAgent?: string | undefined;
|
|
38
|
+
deviceName?: string | undefined;
|
|
39
|
+
deviceType?: string | undefined;
|
|
40
|
+
deviceOS?: string | undefined;
|
|
41
|
+
deviceBrowser?: string | undefined;
|
|
42
|
+
additionalInfo?: JSONObject | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TouchSessionOptions {
|
|
46
|
+
ipAddress?: string | undefined;
|
|
47
|
+
userAgent?: string | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RevokeSessionOptions {
|
|
51
|
+
reason?: string | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class Service extends DatabaseService<Model> {
|
|
55
|
+
private static readonly DEFAULT_REFRESH_TOKEN_TTL_DAYS: number = 30;
|
|
56
|
+
private static readonly SHORT_TEXT_LIMIT: number = 100;
|
|
57
|
+
|
|
58
|
+
public constructor() {
|
|
59
|
+
super(Model);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async createSession(
|
|
63
|
+
options: CreateSessionOptions,
|
|
64
|
+
): Promise<SessionMetadata> {
|
|
65
|
+
const refreshToken: string =
|
|
66
|
+
options.refreshToken || Service.generateRefreshToken();
|
|
67
|
+
const refreshTokenExpiresAt: Date =
|
|
68
|
+
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
|
69
|
+
|
|
70
|
+
const session: Model = this.buildSessionModel(options, {
|
|
71
|
+
refreshToken,
|
|
72
|
+
refreshTokenExpiresAt,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const createdSession: Model = await this.create({
|
|
77
|
+
data: session,
|
|
78
|
+
props: {
|
|
79
|
+
isRoot: true,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
session: createdSession,
|
|
85
|
+
refreshToken: refreshToken,
|
|
86
|
+
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw error as Exception;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async findActiveSessionByRefreshToken(
|
|
94
|
+
refreshToken: string,
|
|
95
|
+
): Promise<Model | null> {
|
|
96
|
+
const hashedValue: string = await HashedString.hashValue(
|
|
97
|
+
refreshToken,
|
|
98
|
+
EncryptionSecret,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return await this.findOneBy({
|
|
102
|
+
query: {
|
|
103
|
+
refreshToken: new HashedString(hashedValue, true),
|
|
104
|
+
isRevoked: false,
|
|
105
|
+
},
|
|
106
|
+
select: {
|
|
107
|
+
_id: true,
|
|
108
|
+
userId: true,
|
|
109
|
+
refreshTokenExpiresAt: true,
|
|
110
|
+
lastActiveAt: true,
|
|
111
|
+
isRevoked: true,
|
|
112
|
+
additionalInfo: true,
|
|
113
|
+
deviceName: true,
|
|
114
|
+
deviceType: true,
|
|
115
|
+
deviceOS: true,
|
|
116
|
+
deviceBrowser: true,
|
|
117
|
+
ipAddress: true,
|
|
118
|
+
userAgent: true,
|
|
119
|
+
},
|
|
120
|
+
props: {
|
|
121
|
+
isRoot: true,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public async renewSessionWithNewRefreshToken(
|
|
127
|
+
options: RenewSessionOptions,
|
|
128
|
+
): Promise<SessionMetadata> {
|
|
129
|
+
const refreshToken: string = Service.generateRefreshToken();
|
|
130
|
+
const refreshTokenExpiresAt: Date =
|
|
131
|
+
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
|
132
|
+
|
|
133
|
+
const updatePayload: Partial<Model> = {
|
|
134
|
+
refreshToken: HashedString.fromString(refreshToken),
|
|
135
|
+
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
|
136
|
+
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
|
137
|
+
isRevoked: false,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const ipAddress: string | undefined = Text.truncate(
|
|
141
|
+
options.ipAddress,
|
|
142
|
+
Service.SHORT_TEXT_LIMIT,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (ipAddress) {
|
|
146
|
+
updatePayload.ipAddress = ipAddress;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options.userAgent) {
|
|
150
|
+
updatePayload.userAgent = options.userAgent;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const deviceName: string | undefined = Text.truncate(
|
|
154
|
+
options.deviceName,
|
|
155
|
+
Service.SHORT_TEXT_LIMIT,
|
|
156
|
+
);
|
|
157
|
+
if (deviceName) {
|
|
158
|
+
updatePayload.deviceName = deviceName;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const deviceType: string | undefined = Text.truncate(
|
|
162
|
+
options.deviceType,
|
|
163
|
+
Service.SHORT_TEXT_LIMIT,
|
|
164
|
+
);
|
|
165
|
+
if (deviceType) {
|
|
166
|
+
updatePayload.deviceType = deviceType;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const deviceOS: string | undefined = Text.truncate(
|
|
170
|
+
options.deviceOS,
|
|
171
|
+
Service.SHORT_TEXT_LIMIT,
|
|
172
|
+
);
|
|
173
|
+
if (deviceOS) {
|
|
174
|
+
updatePayload.deviceOS = deviceOS;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const deviceBrowser: string | undefined = Text.truncate(
|
|
178
|
+
options.deviceBrowser,
|
|
179
|
+
Service.SHORT_TEXT_LIMIT,
|
|
180
|
+
);
|
|
181
|
+
if (deviceBrowser) {
|
|
182
|
+
updatePayload.deviceBrowser = deviceBrowser;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (options.additionalInfo || options.session.additionalInfo) {
|
|
186
|
+
updatePayload.additionalInfo = {
|
|
187
|
+
...(options.session.additionalInfo || {}),
|
|
188
|
+
...(options.additionalInfo || {}),
|
|
189
|
+
} as JSONObject;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const updatedSession: Model | null = await this.updateOneByIdAndFetch({
|
|
193
|
+
id: options.session.id!,
|
|
194
|
+
data: updatePayload as any,
|
|
195
|
+
props: {
|
|
196
|
+
isRoot: true,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!updatedSession) {
|
|
201
|
+
throw new BadDataException("Unable to renew user session");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
session: updatedSession,
|
|
206
|
+
refreshToken: refreshToken,
|
|
207
|
+
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public async touchSession(
|
|
212
|
+
sessionId: ObjectID,
|
|
213
|
+
options: TouchSessionOptions,
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
const updatePayload: Partial<Model> = {
|
|
216
|
+
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const ipAddress: string | undefined = Text.truncate(
|
|
220
|
+
options.ipAddress,
|
|
221
|
+
Service.SHORT_TEXT_LIMIT,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (ipAddress) {
|
|
225
|
+
updatePayload.ipAddress = ipAddress;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (options.userAgent) {
|
|
229
|
+
updatePayload.userAgent = options.userAgent;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await this.updateOneById({
|
|
234
|
+
id: sessionId,
|
|
235
|
+
data: updatePayload as any,
|
|
236
|
+
props: {
|
|
237
|
+
isRoot: true,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
logger.warn(
|
|
242
|
+
`Failed to update session activity timestamp for session ${sessionId.toString()}: ${(err as Error).message}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public async revokeSessionById(
|
|
248
|
+
sessionId: ObjectID,
|
|
249
|
+
options?: RevokeSessionOptions,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
await this.updateOneById({
|
|
252
|
+
id: sessionId,
|
|
253
|
+
data: {
|
|
254
|
+
isRevoked: true,
|
|
255
|
+
revokedAt: OneUptimeDate.getCurrentDate(),
|
|
256
|
+
revokedReason: options?.reason ?? null,
|
|
257
|
+
},
|
|
258
|
+
props: {
|
|
259
|
+
isRoot: true,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public async revokeSessionByRefreshToken(
|
|
265
|
+
refreshToken: string,
|
|
266
|
+
options?: RevokeSessionOptions,
|
|
267
|
+
): Promise<void> {
|
|
268
|
+
const session: Model | null =
|
|
269
|
+
await this.findActiveSessionByRefreshToken(refreshToken);
|
|
270
|
+
|
|
271
|
+
if (!session || !session.id) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await this.revokeSessionById(session.id, options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private buildSessionModel(
|
|
279
|
+
options: CreateSessionOptions,
|
|
280
|
+
tokenMeta: { refreshToken: string; refreshTokenExpiresAt: Date },
|
|
281
|
+
): Model {
|
|
282
|
+
const session: Model = new Model();
|
|
283
|
+
session.userId = options.userId;
|
|
284
|
+
session.refreshToken = HashedString.fromString(tokenMeta.refreshToken);
|
|
285
|
+
session.refreshTokenExpiresAt = tokenMeta.refreshTokenExpiresAt;
|
|
286
|
+
session.lastActiveAt = OneUptimeDate.getCurrentDate();
|
|
287
|
+
if (options.userAgent) {
|
|
288
|
+
session.userAgent = options.userAgent;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const deviceName: string | undefined = Text.truncate(
|
|
292
|
+
options.deviceName,
|
|
293
|
+
Service.SHORT_TEXT_LIMIT,
|
|
294
|
+
);
|
|
295
|
+
if (deviceName) {
|
|
296
|
+
session.deviceName = deviceName;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const deviceType: string | undefined = Text.truncate(
|
|
300
|
+
options.deviceType,
|
|
301
|
+
Service.SHORT_TEXT_LIMIT,
|
|
302
|
+
);
|
|
303
|
+
if (deviceType) {
|
|
304
|
+
session.deviceType = deviceType;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const deviceOS: string | undefined = Text.truncate(
|
|
308
|
+
options.deviceOS,
|
|
309
|
+
Service.SHORT_TEXT_LIMIT,
|
|
310
|
+
);
|
|
311
|
+
if (deviceOS) {
|
|
312
|
+
session.deviceOS = deviceOS;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const deviceBrowser: string | undefined = Text.truncate(
|
|
316
|
+
options.deviceBrowser,
|
|
317
|
+
Service.SHORT_TEXT_LIMIT,
|
|
318
|
+
);
|
|
319
|
+
if (deviceBrowser) {
|
|
320
|
+
session.deviceBrowser = deviceBrowser;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const ipAddress: string | undefined = Text.truncate(
|
|
324
|
+
options.ipAddress,
|
|
325
|
+
Service.SHORT_TEXT_LIMIT,
|
|
326
|
+
);
|
|
327
|
+
if (ipAddress) {
|
|
328
|
+
session.ipAddress = ipAddress;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
session.additionalInfo = {
|
|
332
|
+
...(options.additionalInfo || {}),
|
|
333
|
+
isGlobalLogin: options.isGlobalLogin,
|
|
334
|
+
} as JSONObject;
|
|
335
|
+
|
|
336
|
+
return session;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private static generateRefreshToken(): string {
|
|
340
|
+
return ObjectID.generate().toString();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private static getRefreshTokenExpiry(): Date {
|
|
344
|
+
return OneUptimeDate.getSomeDaysAfter(
|
|
345
|
+
Service.DEFAULT_REFRESH_TOKEN_TTL_DAYS,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export default new Service();
|