@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/index.ts CHANGED
@@ -1 +1 @@
1
- export * from "./src"
1
+ export * from './src';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomei/sso",
3
- "version": "0.3.4",
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
- //validate email
138
- if (this.Email !== email) {
139
- throw new Error('Invalid credentials.');
140
- }
137
+ try {
138
+ //validate email
139
+ if (this.Email !== email) {
140
+ throw new Error('Invalid credentials.');
141
+ }
141
142
 
142
- //validate password
143
- const isPasswordValid = await this._PasswordHashService.verify(
144
- password,
145
- this.Password,
146
- );
143
+ //validate password
144
+ const isPasswordValid = await this._PasswordHashService.verify(
145
+ password,
146
+ this.Password,
147
+ );
147
148
 
148
- if (!isPasswordValid) {
149
- throw new Error('Invalid credentials.');
150
- }
149
+ if (!isPasswordValid) {
150
+ throw new Error('Invalid credentials.');
151
+ }
151
152
 
152
- //validate system code
153
- const system = await LoginUser._SystemRepository.findOne({
154
- where: {
155
- code: systemCode,
156
- },
157
- });
153
+ //validate system code
154
+ const system = await LoginUser._SystemRepository.findOne({
155
+ where: {
156
+ code: systemCode,
157
+ },
158
+ });
158
159
 
159
- if (!system) {
160
- throw new Error('Invalid system code.');
161
- }
160
+ if (!system) {
161
+ throw new Error('Invalid system code.');
162
+ }
162
163
 
163
- //validate system access
164
- await this.checkSystemAccess(this.ObjectId, system.id);
165
- // alert user if new login
166
- await this.alertNewLogin(this.ObjectId, system.id, ipAddress);
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
- // fetch user session if exists
169
- const userSession = await this._SessionService.retrieveUserSession(
170
- this.ObjectId,
171
- );
172
- let systemLogin = userSession.systemLogins.find(
173
- (system) => system.code === systemCode,
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
- // generate new session id
177
- const { randomUUID } = require('crypto');
178
- const sessionId = randomUUID();
177
+ // generate new session id
178
+ const { randomUUID } = require('crypto');
179
+ const sessionId = randomUUID();
179
180
 
180
- if (systemLogin) {
181
- systemLogin = systemLogin.sessionId = sessionId;
182
- userSession.systemLogins.map((system) =>
183
- system.code === systemCode ? systemLogin : system,
184
- );
185
- } else {
186
- // if not, add new system login into the userSession
187
- const newLogin = {
188
- id: system.id.toString(),
189
- code: system.code,
190
- sessionId: sessionId,
191
- privileges: await this.getPrivileges(system.code),
192
- };
193
- userSession.systemLogins.push(newLogin);
194
- }
195
- // then update userSession inside the redis storage with 1 day duration of time-to-live
196
- this._SessionService.setUserSession(this.ObjectId, userSession);
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
- return sessionId;
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
- const userLogins = await LoginUser._LoginHistoryRepository.findAll({
233
- where: {
234
- userId: userId,
235
- systemId: systemId,
236
- },
237
- });
236
+ try {
237
+ const userLogins = await LoginUser._LoginHistoryRepository.findAll({
238
+ where: {
239
+ userId: userId,
240
+ systemId: systemId,
241
+ },
242
+ });
238
243
 
239
- const gotPreviousLogins = userLogins?.length !== 0;
240
- let ipFound = null;
241
- if (gotPreviousLogins) {
242
- ipFound = userLogins.find((item) => item.ipAddress === ipAddress);
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
- if (gotPreviousLogins && !ipFound) {
246
- await this._MailService.sendNewLoginAlertEmail({
247
- IpAddress: ipAddress,
248
- Email: this.Email,
249
- Name: this.FullName,
250
- LoginDate: new Date(),
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.parentCode,
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 = userRole.userSystemPrivileges.filter(
421
- (u) => u.systemPrivilege.systemId === systemId,
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
@@ -2,6 +2,5 @@ require('dotenv').config();
2
2
  export * from './components';
3
3
  export * from './interfaces';
4
4
  export * from './redis-client';
5
- export * from './mail';
6
5
  export * from './prisma-client';
7
6
  export * from './session';
@@ -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
@@ -1,2 +0,0 @@
1
- export * from './mail.service';
2
- export * from './mail';
@@ -1,2 +0,0 @@
1
- export * from './send-mail.interface';
2
- export * from './send-new-login-alert.interface';
@@ -1,8 +0,0 @@
1
- export interface ISendMailOptionsInterface {
2
- to: string | string[];
3
- cc?: string | string[];
4
- bcc?: string | string[];
5
- subject: string;
6
- text?: string;
7
- html: string;
8
- }
@@ -1,6 +0,0 @@
1
- export interface ISendNewLoginAlertOptions {
2
- IpAddress: string;
3
- Email: string;
4
- Name: string;
5
- LoginDate: Date;
6
- }
@@ -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
- }