@tomei/sso 0.3.4 → 0.6.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/__tests__/unit/components/login-user/login-user.spec.ts +530 -59
- package/dist/__tests__/unit/components/login-user/login-user.spec.js +465 -45
- package/dist/__tests__/unit/components/login-user/login-user.spec.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/components/login-user/login-user.d.ts +4 -2
- package/dist/src/components/login-user/login-user.js +131 -63
- package/dist/src/components/login-user/login-user.js.map +1 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/session/interfaces/session-service.interface.d.ts +1 -0
- package/dist/src/session/session.service.d.ts +1 -0
- package/dist/src/session/session.service.js +12 -0
- package/dist/src/session/session.service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +1 -1
- package/package.json +4 -1
- package/src/components/login-user/login-user.ts +179 -89
- package/src/index.ts +0 -1
- package/src/session/interfaces/session-service.interface.ts +1 -0
- package/src/session/session.service.ts +10 -0
- package/src/mail/index.ts +0 -2
- package/src/mail/interfaces/index.ts +0 -2
- package/src/mail/interfaces/send-mail.interface.ts +0 -8
- package/src/mail/interfaces/send-new-login-alert.interface.ts +0 -6
- package/src/mail/mail.service.ts +0 -33
- package/src/mail/mail.ts +0 -40
package/index.ts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export * from
|
1
|
+
export * from './src';
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@tomei/sso",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.6.0",
|
4
4
|
"description": "Tomei SSO Package",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"scripts": {
|
@@ -70,5 +70,8 @@
|
|
70
70
|
"prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
71
71
|
"eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
|
72
72
|
]
|
73
|
+
},
|
74
|
+
"dependencies": {
|
75
|
+
"@tomei/mailer": "^0.1.3"
|
73
76
|
}
|
74
77
|
}
|
@@ -6,12 +6,13 @@ import { UserRepository } from './user.repository';
|
|
6
6
|
import { SystemRepository } from '../system/system.repository';
|
7
7
|
import { SystemAccessRepository } from '../system-access/system-access.repository';
|
8
8
|
import { LoginHistoryRepository } from '../login-history/login-history.repository';
|
9
|
-
import { MailService } from '../../mail/mail.service';
|
10
9
|
import { UserUserGroupRepository } from '../user-user-group/user-user-group.repository';
|
11
10
|
import { PasswordHashService } from '../password-hash/password-hash.service';
|
12
11
|
import { SessionService } from '../../session/session.service';
|
13
12
|
import { UserGroupRepository } from '../user-group/user-group.repository';
|
14
|
-
|
13
|
+
import * as nodemailer from 'nodemailer';
|
14
|
+
import { Mailer } from '@tomei/mailer';
|
15
|
+
import { ISystemLogin } from '../../../src/interfaces/system-login.interface';
|
15
16
|
export class LoginUser extends ObjectBase implements IPerson {
|
16
17
|
FullName: string;
|
17
18
|
IDNo: string;
|
@@ -28,7 +29,6 @@ export class LoginUser extends ObjectBase implements IPerson {
|
|
28
29
|
private _OriginIP: string;
|
29
30
|
private _SessionService: ISessionService;
|
30
31
|
private _PasswordHashService = new PasswordHashService();
|
31
|
-
private _MailService = new MailService();
|
32
32
|
private static _Repository = new UserRepository();
|
33
33
|
private static _SystemRepository = new SystemRepository();
|
34
34
|
private static _SystemAccessRepository = new SystemAccessRepository();
|
@@ -134,78 +134,82 @@ export class LoginUser extends ObjectBase implements IPerson {
|
|
134
134
|
password: string,
|
135
135
|
ipAddress: string,
|
136
136
|
): Promise<string> {
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
137
|
+
try {
|
138
|
+
//validate email
|
139
|
+
if (this.Email !== email) {
|
140
|
+
throw new Error('Invalid credentials.');
|
141
|
+
}
|
141
142
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
143
|
+
//validate password
|
144
|
+
const isPasswordValid = await this._PasswordHashService.verify(
|
145
|
+
password,
|
146
|
+
this.Password,
|
147
|
+
);
|
147
148
|
|
148
|
-
|
149
|
-
|
150
|
-
|
149
|
+
if (!isPasswordValid) {
|
150
|
+
throw new Error('Invalid credentials.');
|
151
|
+
}
|
151
152
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
153
|
+
//validate system code
|
154
|
+
const system = await LoginUser._SystemRepository.findOne({
|
155
|
+
where: {
|
156
|
+
code: systemCode,
|
157
|
+
},
|
158
|
+
});
|
158
159
|
|
159
|
-
|
160
|
-
|
161
|
-
|
160
|
+
if (!system) {
|
161
|
+
throw new Error('Invalid system code.');
|
162
|
+
}
|
162
163
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
164
|
+
//validate system access
|
165
|
+
await this.checkSystemAccess(this.ObjectId, system.id);
|
166
|
+
// alert user if new login
|
167
|
+
await this.alertNewLogin(this.ObjectId, system.id, ipAddress);
|
167
168
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
169
|
+
// fetch user session if exists
|
170
|
+
const userSession = await this._SessionService.retrieveUserSession(
|
171
|
+
this.ObjectId,
|
172
|
+
);
|
173
|
+
let systemLogin = userSession.systemLogins.find(
|
174
|
+
(system) => system.code === systemCode,
|
175
|
+
);
|
175
176
|
|
176
|
-
|
177
|
-
|
178
|
-
|
177
|
+
// generate new session id
|
178
|
+
const { randomUUID } = require('crypto');
|
179
|
+
const sessionId = randomUUID();
|
179
180
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
// record new login history
|
199
|
-
await LoginUser._LoginHistoryRepository.create({
|
200
|
-
data: {
|
201
|
-
userId: this.ObjectId,
|
202
|
-
systemId: system.id,
|
203
|
-
originIp: ipAddress,
|
204
|
-
createdAt: new Date(),
|
205
|
-
},
|
206
|
-
});
|
181
|
+
if (systemLogin) {
|
182
|
+
systemLogin = systemLogin.sessionId = sessionId;
|
183
|
+
userSession.systemLogins.map((system) =>
|
184
|
+
system.code === systemCode ? systemLogin : system,
|
185
|
+
);
|
186
|
+
} else {
|
187
|
+
// if not, add new system login into the userSession
|
188
|
+
const newLogin = {
|
189
|
+
id: system.id.toString(),
|
190
|
+
code: system.code,
|
191
|
+
sessionId: sessionId,
|
192
|
+
privileges: await this.getPrivileges(system.code),
|
193
|
+
};
|
194
|
+
userSession.systemLogins.push(newLogin);
|
195
|
+
}
|
196
|
+
// then update userSession inside the redis storage with 1 day duration of time-to-live
|
197
|
+
this._SessionService.setUserSession(this.ObjectId, userSession);
|
207
198
|
|
208
|
-
|
199
|
+
// record new login history
|
200
|
+
await LoginUser._LoginHistoryRepository.create({
|
201
|
+
data: {
|
202
|
+
userId: this.ObjectId,
|
203
|
+
systemId: system.id,
|
204
|
+
originIp: ipAddress,
|
205
|
+
createdAt: new Date(),
|
206
|
+
},
|
207
|
+
});
|
208
|
+
|
209
|
+
return sessionId;
|
210
|
+
} catch (error) {
|
211
|
+
throw error;
|
212
|
+
}
|
209
213
|
}
|
210
214
|
|
211
215
|
private async checkSystemAccess(
|
@@ -229,30 +233,57 @@ export class LoginUser extends ObjectBase implements IPerson {
|
|
229
233
|
systemId: string,
|
230
234
|
ipAddress: string,
|
231
235
|
) {
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
236
|
+
try {
|
237
|
+
const userLogins = await LoginUser._LoginHistoryRepository.findAll({
|
238
|
+
where: {
|
239
|
+
userId: userId,
|
240
|
+
systemId: systemId,
|
241
|
+
},
|
242
|
+
});
|
238
243
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
+
const gotPreviousLogins = userLogins?.length !== 0;
|
245
|
+
let ipFound = null;
|
246
|
+
if (gotPreviousLogins) {
|
247
|
+
ipFound = userLogins.find((item) => item.ipAddress === ipAddress);
|
248
|
+
}
|
244
249
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
250
|
+
if (gotPreviousLogins && !ipFound) {
|
251
|
+
const SMTP_HOST = process.env.SMTP_HOST || '';
|
252
|
+
const SMTP_PORT = process.env.SMTP_PORT || '';
|
253
|
+
const EMAIL_SENDER = process.env.EMAIL_SENDER || '';
|
254
|
+
const EMAIL_PASSWORD = process.env.EMAIL_PASSWORD || '';
|
255
|
+
|
256
|
+
const mailer = new Mailer(nodemailer, {
|
257
|
+
host: SMTP_HOST,
|
258
|
+
port: Number(SMTP_PORT),
|
259
|
+
secure: Number(SMTP_PORT) === 465,
|
260
|
+
auth: {
|
261
|
+
user: EMAIL_SENDER,
|
262
|
+
pass: EMAIL_PASSWORD,
|
263
|
+
},
|
264
|
+
});
|
265
|
+
|
266
|
+
await mailer.sendMail({
|
267
|
+
from: process.env.EMAIL_SENDER,
|
268
|
+
to: this.Email,
|
269
|
+
subject: 'New Login Alert',
|
270
|
+
html: `<p>Dear ${this.FullName},</p>
|
271
|
+
<p>There was a new login to your account from ${ipAddress} on ${new Date().toLocaleString()}.</p>
|
272
|
+
<p>If this was you, you can safely ignore this email.</p>
|
273
|
+
<p>If you suspect that someone else is trying to access your account, please contact us immediately at itd-support@tomei.com.my.</p>
|
274
|
+
<p>Thank you!,</p>
|
275
|
+
<p>
|
276
|
+
Best Regards,
|
277
|
+
IT Department
|
278
|
+
</p>`,
|
279
|
+
});
|
280
|
+
}
|
281
|
+
} catch (error) {
|
282
|
+
throw error;
|
252
283
|
}
|
253
284
|
}
|
254
285
|
|
255
|
-
async getPrivileges(systemCode: string): Promise<string[]> {
|
286
|
+
private async getPrivileges(systemCode: string): Promise<string[]> {
|
256
287
|
try {
|
257
288
|
const system = await LoginUser._SystemRepository.findOne({
|
258
289
|
where: {
|
@@ -263,12 +294,12 @@ export class LoginUser extends ObjectBase implements IPerson {
|
|
263
294
|
if (!system) {
|
264
295
|
throw new Error('Invalid system code.');
|
265
296
|
}
|
297
|
+
|
266
298
|
// retrive user userGroups with system privileges
|
267
299
|
const userUserGroups = await this.getUserUserGroupFromDB(system.id);
|
268
300
|
|
269
301
|
// get all userGroup data from user userGroups
|
270
302
|
const userGroupData = userUserGroups.map((u) => u.userGroup);
|
271
|
-
|
272
303
|
// get all privileges from userGroup data
|
273
304
|
let privileges: string[] = [];
|
274
305
|
for (const userGroup of userGroupData) {
|
@@ -286,7 +317,7 @@ export class LoginUser extends ObjectBase implements IPerson {
|
|
286
317
|
) {
|
287
318
|
// get all parent tree privileges
|
288
319
|
const parentTreePrivileges = await this.getPrivilegesFromUserGroup(
|
289
|
-
userGroup.
|
320
|
+
userGroup.parentGroupCode,
|
290
321
|
);
|
291
322
|
|
292
323
|
privileges = [...privileges, ...parentTreePrivileges];
|
@@ -417,8 +448,8 @@ export class LoginUser extends ObjectBase implements IPerson {
|
|
417
448
|
(u) => u.systemPrivilege,
|
418
449
|
);
|
419
450
|
|
420
|
-
userSystemPrivileges =
|
421
|
-
(u) => u.
|
451
|
+
userSystemPrivileges = userSystemPrivileges.filter(
|
452
|
+
(u) => u.systemId === systemId,
|
422
453
|
);
|
423
454
|
|
424
455
|
const userPrivileges: string[] = userSystemPrivileges.map((u) => u.code);
|
@@ -427,4 +458,63 @@ export class LoginUser extends ObjectBase implements IPerson {
|
|
427
458
|
throw error;
|
428
459
|
}
|
429
460
|
}
|
461
|
+
|
462
|
+
async checkPrivileges(
|
463
|
+
systemCode: string,
|
464
|
+
privilegeName: string,
|
465
|
+
): Promise<boolean> {
|
466
|
+
if (!this.ObjectId) {
|
467
|
+
throw new Error('ObjectId(UserId) is not set');
|
468
|
+
}
|
469
|
+
|
470
|
+
const userSession = await this._SessionService.retrieveUserSession(
|
471
|
+
this.ObjectId,
|
472
|
+
);
|
473
|
+
|
474
|
+
const systemLogin = userSession.systemLogins.find(
|
475
|
+
(system) => system.code === systemCode,
|
476
|
+
);
|
477
|
+
|
478
|
+
if (!systemLogin) {
|
479
|
+
return false;
|
480
|
+
}
|
481
|
+
|
482
|
+
const privileges = systemLogin.privileges;
|
483
|
+
const hasPrivilege = privileges.includes(privilegeName);
|
484
|
+
return hasPrivilege;
|
485
|
+
}
|
486
|
+
|
487
|
+
async checkSession(
|
488
|
+
systemCode: string,
|
489
|
+
sessionId: string,
|
490
|
+
userId: string,
|
491
|
+
): Promise<ISystemLogin> {
|
492
|
+
try {
|
493
|
+
const userSession = await this._SessionService.retrieveUserSession(
|
494
|
+
userId,
|
495
|
+
);
|
496
|
+
|
497
|
+
if (userSession.systemLogins.length === 0) {
|
498
|
+
throw new Error('Session expired.');
|
499
|
+
}
|
500
|
+
|
501
|
+
const systemLogin = userSession.systemLogins.find(
|
502
|
+
(sl) => sl.code === systemCode,
|
503
|
+
);
|
504
|
+
|
505
|
+
if (!systemLogin) {
|
506
|
+
throw new Error('Session expired.');
|
507
|
+
}
|
508
|
+
|
509
|
+
if (systemLogin.sessionId !== sessionId) {
|
510
|
+
throw new Error('Session expired.');
|
511
|
+
}
|
512
|
+
|
513
|
+
await this._SessionService.refreshDuration(userId);
|
514
|
+
|
515
|
+
return systemLogin;
|
516
|
+
} catch (error) {
|
517
|
+
throw error;
|
518
|
+
}
|
519
|
+
}
|
430
520
|
}
|
package/src/index.ts
CHANGED
@@ -3,4 +3,5 @@ import { IUserSession } from '../../interfaces/user-session.interface';
|
|
3
3
|
export interface ISessionService {
|
4
4
|
retrieveUserSession(userId: string): Promise<IUserSession>;
|
5
5
|
setUserSession(userId: string, sessionData: IUserSession): Promise<void>;
|
6
|
+
refreshDuration(userId: string): Promise<void>;
|
6
7
|
}
|
@@ -42,4 +42,14 @@ export class SessionService implements ISessionService {
|
|
42
42
|
throw error;
|
43
43
|
}
|
44
44
|
}
|
45
|
+
|
46
|
+
async refreshDuration(userid: string): Promise<void> {
|
47
|
+
try {
|
48
|
+
const key = `tomei-sid:${userid}`;
|
49
|
+
const userSession = await this.retrieveUserSession(userid);
|
50
|
+
await this.setUserSession(key, userSession);
|
51
|
+
} catch (error) {
|
52
|
+
throw error;
|
53
|
+
}
|
54
|
+
}
|
45
55
|
}
|
package/src/mail/index.ts
DELETED
package/src/mail/mail.service.ts
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
import { ISendNewLoginAlertOptions } from './interfaces/send-new-login-alert.interface';
|
2
|
-
import Mail from './mail';
|
3
|
-
|
4
|
-
export class MailService {
|
5
|
-
MailClient: Mail;
|
6
|
-
constructor() {
|
7
|
-
this.MailClient = Mail.init();
|
8
|
-
}
|
9
|
-
|
10
|
-
public async sendNewLoginAlertEmail(
|
11
|
-
options: ISendNewLoginAlertOptions,
|
12
|
-
): Promise<void> {
|
13
|
-
try {
|
14
|
-
await this.MailClient.sendMail({
|
15
|
-
to: options.Email,
|
16
|
-
subject: 'New Login Alert',
|
17
|
-
html: `<p>Dear ${options.Name},</p>
|
18
|
-
<p>There was a new login to your account from ${
|
19
|
-
options.IpAddress
|
20
|
-
} on ${options.LoginDate.toLocaleString()}.</p>
|
21
|
-
<p>If this was you, you can safely ignore this email.</p>
|
22
|
-
<p>If you suspect that someone else is trying to access your account, please contact us immediately at itd-support@tomei.com.my.</p>
|
23
|
-
<p>Thank you!,</p>
|
24
|
-
<p>
|
25
|
-
Best Regards,
|
26
|
-
IT Department
|
27
|
-
</p>`,
|
28
|
-
});
|
29
|
-
} catch (error) {
|
30
|
-
throw error;
|
31
|
-
}
|
32
|
-
}
|
33
|
-
}
|
package/src/mail/mail.ts
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
import * as nodemailer from 'nodemailer';
|
2
|
-
import { ISendMailOptionsInterface } from './interfaces/send-mail.interface';
|
3
|
-
|
4
|
-
export default class Mail {
|
5
|
-
private static transporter: nodemailer.Transporter;
|
6
|
-
|
7
|
-
constructor() {}
|
8
|
-
|
9
|
-
static init() {
|
10
|
-
if (!Mail.transporter) {
|
11
|
-
Mail.transporter = nodemailer.createTransport({
|
12
|
-
host: process.env.SMTP_HOST,
|
13
|
-
port: Number(process.env.SMTP_PORT),
|
14
|
-
secure: Number(process.env.SMTP_PORT) === 465,
|
15
|
-
auth: {
|
16
|
-
user: process.env.EMAIL_SENDER,
|
17
|
-
pass: process.env.EMAIL_PASSWORD,
|
18
|
-
},
|
19
|
-
});
|
20
|
-
}
|
21
|
-
return new Mail();
|
22
|
-
}
|
23
|
-
|
24
|
-
//SEND MAIL
|
25
|
-
async sendMail(options: ISendMailOptionsInterface): Promise<void> {
|
26
|
-
try {
|
27
|
-
await Mail.transporter.sendMail({
|
28
|
-
from: process.env.EMAIL_SENDER,
|
29
|
-
to: options.to,
|
30
|
-
cc: options.cc,
|
31
|
-
bcc: options.bcc,
|
32
|
-
subject: options.subject,
|
33
|
-
text: options.text,
|
34
|
-
html: options.html,
|
35
|
-
});
|
36
|
-
} catch (error) {
|
37
|
-
throw error;
|
38
|
-
}
|
39
|
-
}
|
40
|
-
}
|