@flusys/nestjs-auth 3.0.0 → 4.0.0-rc
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/README.md +1 -1
- package/cjs/config/index.js +1 -0
- package/cjs/config/message-keys.js +150 -0
- package/cjs/controllers/authentication.controller.js +9 -4
- package/cjs/controllers/branch.controller.js +1 -0
- package/cjs/controllers/company-selection.controller.js +3 -0
- package/cjs/controllers/company.controller.js +1 -0
- package/cjs/controllers/email-verification.controller.js +7 -2
- package/cjs/controllers/user-permission.controller.js +9 -1
- package/cjs/controllers/user.controller.js +16 -4
- package/cjs/dtos/authentication.dto.js +8 -0
- package/cjs/dtos/user-permission.dto.js +14 -0
- package/cjs/interceptors/set-token.interceptor.js +14 -5
- package/cjs/services/auth-config.service.js +9 -8
- package/cjs/services/auth-datasource.provider.js +7 -4
- package/cjs/services/auth-email.service.js +155 -45
- package/cjs/services/authentication.service.js +306 -71
- package/cjs/services/branch.service.js +1 -1
- package/cjs/services/company-selection-session.service.js +25 -6
- package/cjs/services/company.service.js +1 -1
- package/cjs/services/user-permission.service.js +52 -4
- package/cjs/services/user.service.js +122 -26
- package/cjs/strategies/jwt.strategy.js +18 -4
- package/config/index.d.ts +1 -0
- package/config/message-keys.d.ts +218 -0
- package/controllers/company-selection.controller.d.ts +2 -0
- package/controllers/user-permission.controller.d.ts +4 -0
- package/controllers/user.controller.d.ts +10 -0
- package/dtos/authentication.dto.d.ts +1 -0
- package/dtos/user-permission.dto.d.ts +2 -0
- package/entities/index.d.ts +10 -1
- package/fesm/config/index.js +1 -0
- package/fesm/config/message-keys.js +118 -0
- package/fesm/controllers/authentication.controller.js +9 -3
- package/fesm/controllers/branch.controller.js +1 -0
- package/fesm/controllers/company-selection.controller.js +3 -0
- package/fesm/controllers/company.controller.js +1 -0
- package/fesm/controllers/email-verification.controller.js +7 -2
- package/fesm/controllers/user-permission.controller.js +9 -1
- package/fesm/controllers/user.controller.js +16 -4
- package/fesm/dtos/authentication.dto.js +8 -0
- package/fesm/dtos/user-permission.dto.js +14 -0
- package/fesm/interceptors/set-token.interceptor.js +14 -4
- package/fesm/services/auth-config.service.js +10 -9
- package/fesm/services/auth-datasource.provider.js +7 -4
- package/fesm/services/auth-email.service.js +156 -45
- package/fesm/services/authentication.service.js +297 -61
- package/fesm/services/branch.service.js +1 -1
- package/fesm/services/company-selection-session.service.js +25 -6
- package/fesm/services/company.service.js +1 -1
- package/fesm/services/user-permission.service.js +52 -4
- package/fesm/services/user.service.js +122 -25
- package/fesm/strategies/jwt.strategy.js +18 -4
- package/interfaces/auth-module-options.interface.d.ts +2 -2
- package/interfaces/authentication.interface.d.ts +1 -0
- package/modules/auth.module.d.ts +2 -1
- package/package.json +3 -3
- package/services/auth-config.service.d.ts +0 -2
- package/services/auth-datasource.provider.d.ts +1 -3
- package/services/auth-email.service.d.ts +6 -3
- package/services/authentication.service.d.ts +5 -2
- package/services/branch.service.d.ts +2 -1
- package/services/user.service.d.ts +7 -7
|
@@ -26,14 +26,21 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
29
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
30
|
+
import { LogAction } from '@flusys/nestjs-shared';
|
|
31
|
+
import { AUTH_MESSAGES, EMAIL_VERIFICATION_MESSAGES, USER_MESSAGES } from '../config';
|
|
32
|
+
import { INotificationAdapter, NOTIFICATION_ADAPTER } from '@flusys/nestjs-shared/interfaces';
|
|
33
|
+
import { NotificationType } from '@flusys/nestjs-shared/enums';
|
|
34
|
+
import { generateUniqueSlug } from '@flusys/nestjs-shared/utils';
|
|
35
|
+
import { BadRequestException, Inject, Injectable, InternalServerErrorException, NotFoundException, Optional, Scope, UnauthorizedException } from '@nestjs/common';
|
|
31
36
|
import { JwtService } from '@nestjs/jwt';
|
|
32
37
|
import * as bcrypt from 'bcrypt';
|
|
33
38
|
import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX, USER_ENRICHER } from '../config';
|
|
34
39
|
import { IUserEnricher } from '../interfaces/user-enricher.interface';
|
|
40
|
+
import { RegistrationDto } from '../dtos/authentication.dto';
|
|
35
41
|
import { CompanyBranch } from '../entities/company-branch.entity';
|
|
36
42
|
import { Company } from '../entities/company.entity';
|
|
43
|
+
import { PendingRegistration } from '../entities/pending-registration.entity';
|
|
37
44
|
import { UserCompanyPermission } from '../entities/user-company-permission.entity';
|
|
38
45
|
import { User } from '../entities/user.entity';
|
|
39
46
|
import { PermissionType } from '../enums';
|
|
@@ -52,7 +59,10 @@ export class AuthenticationService {
|
|
|
52
59
|
}
|
|
53
60
|
async getUserRepository() {
|
|
54
61
|
if (!this.dataSourceProvider) {
|
|
55
|
-
throw new
|
|
62
|
+
throw new InternalServerErrorException({
|
|
63
|
+
message: 'User repository not available',
|
|
64
|
+
messageKey: SYSTEM_MESSAGES.REPOSITORY_NOT_AVAILABLE
|
|
65
|
+
});
|
|
56
66
|
}
|
|
57
67
|
return this.dataSourceProvider.getRepository(User);
|
|
58
68
|
}
|
|
@@ -73,24 +83,29 @@ export class AuthenticationService {
|
|
|
73
83
|
return {
|
|
74
84
|
requiresEmailVerification: true,
|
|
75
85
|
email,
|
|
76
|
-
message: 'Please verify your email address to complete registration.'
|
|
86
|
+
message: 'Please verify your email address to complete registration.',
|
|
87
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_SENT
|
|
77
88
|
};
|
|
78
89
|
}
|
|
79
90
|
const user = await this.validateUser(email, password);
|
|
80
91
|
if (!user.isActive) {
|
|
81
|
-
throw new UnauthorizedException(
|
|
92
|
+
throw new UnauthorizedException({
|
|
93
|
+
message: 'User account is deactivated',
|
|
94
|
+
messageKey: AUTH_MESSAGES.LOGIN_ACCOUNT_DEACTIVATED
|
|
95
|
+
});
|
|
82
96
|
}
|
|
83
97
|
if (this.isEmailVerificationEnabled() && !user.emailVerified) {
|
|
84
98
|
return {
|
|
85
99
|
requiresEmailVerification: true,
|
|
86
100
|
email: user.email,
|
|
87
|
-
message: 'Please verify your email address before logging in.'
|
|
101
|
+
message: 'Please verify your email address before logging in.',
|
|
102
|
+
messageKey: AUTH_MESSAGES.LOGIN_EMAIL_NOT_VERIFIED
|
|
88
103
|
};
|
|
89
104
|
}
|
|
90
105
|
if (!this.isCompanyFeatureEnabled()) {
|
|
91
106
|
return this.generateLoginResponse(user);
|
|
92
107
|
}
|
|
93
|
-
return this.handleCompanyBasedLogin(user);
|
|
108
|
+
return await this.handleCompanyBasedLogin(user);
|
|
94
109
|
}
|
|
95
110
|
async handleCompanyBasedLogin(user) {
|
|
96
111
|
const permissionRepository = await this.getPermissionRepository();
|
|
@@ -142,10 +157,13 @@ export class AuthenticationService {
|
|
|
142
157
|
}
|
|
143
158
|
});
|
|
144
159
|
if (existingUser && this.isCompanyFeatureEnabled()) {
|
|
145
|
-
return this.handleExistingUserCompanyRegistration(existingUser, dto);
|
|
160
|
+
return await this.handleExistingUserCompanyRegistration(existingUser, dto);
|
|
146
161
|
}
|
|
147
162
|
if (existingUser) {
|
|
148
|
-
throw new BadRequestException(
|
|
163
|
+
throw new BadRequestException({
|
|
164
|
+
message: 'Registration failed due to email exists. Please try again or contact support.',
|
|
165
|
+
messageKey: AUTH_MESSAGES.REGISTER_EMAIL_EXISTS
|
|
166
|
+
});
|
|
149
167
|
}
|
|
150
168
|
if (this.isEmailVerificationEnabled() && verifyUrlBase) {
|
|
151
169
|
const companyData = this.isCompanyFeatureEnabled() ? {
|
|
@@ -166,19 +184,26 @@ export class AuthenticationService {
|
|
|
166
184
|
});
|
|
167
185
|
return {
|
|
168
186
|
requiresEmailVerification: true,
|
|
169
|
-
message: result.message
|
|
187
|
+
message: result.message,
|
|
188
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_SENT
|
|
170
189
|
};
|
|
171
190
|
}
|
|
172
|
-
return this.createUserDirectly(dto);
|
|
191
|
+
return await this.createUserDirectly(dto);
|
|
173
192
|
}
|
|
174
193
|
async handleExistingUserCompanyRegistration(existingUser, dto) {
|
|
175
194
|
if (!dto.companySlug && !dto.newCompanyName) {
|
|
176
|
-
throw new BadRequestException(
|
|
195
|
+
throw new BadRequestException({
|
|
196
|
+
message: 'Email already registered. Please login or provide a company to join/create.',
|
|
197
|
+
messageKey: AUTH_MESSAGES.REGISTER_EMAIL_EXISTS
|
|
198
|
+
});
|
|
177
199
|
}
|
|
178
200
|
const permissionRepository = await this.getPermissionRepository();
|
|
179
201
|
const companyRepository = await this.getCompanyRepository();
|
|
180
202
|
if (!permissionRepository || !companyRepository) {
|
|
181
|
-
throw new BadRequestException(
|
|
203
|
+
throw new BadRequestException({
|
|
204
|
+
message: 'Company feature repositories not available',
|
|
205
|
+
messageKey: SYSTEM_MESSAGES.REPOSITORY_NOT_AVAILABLE
|
|
206
|
+
});
|
|
182
207
|
}
|
|
183
208
|
if (dto.companySlug) {
|
|
184
209
|
const company = await companyRepository.findOne({
|
|
@@ -196,15 +221,21 @@ export class AuthenticationService {
|
|
|
196
221
|
}
|
|
197
222
|
});
|
|
198
223
|
if (existingPermission) {
|
|
199
|
-
throw new BadRequestException(
|
|
224
|
+
throw new BadRequestException({
|
|
225
|
+
message: 'You are already assigned to this company. Please login.',
|
|
226
|
+
messageKey: AUTH_MESSAGES.REGISTER_COMPANY_ALREADY_ASSIGNED
|
|
227
|
+
});
|
|
200
228
|
}
|
|
201
229
|
}
|
|
202
230
|
}
|
|
203
|
-
return this.processCompanyRegistration(existingUser, dto, true);
|
|
231
|
+
return await this.processCompanyRegistration(existingUser, dto, true);
|
|
204
232
|
}
|
|
205
233
|
async createUserDirectly(dto) {
|
|
206
234
|
if (!this.dataSourceProvider) {
|
|
207
|
-
throw new BadRequestException(
|
|
235
|
+
throw new BadRequestException({
|
|
236
|
+
message: 'Data source not available',
|
|
237
|
+
messageKey: SYSTEM_MESSAGES.DATASOURCE_NOT_AVAILABLE
|
|
238
|
+
});
|
|
208
239
|
}
|
|
209
240
|
const dataSource = await this.dataSourceProvider.getDataSource();
|
|
210
241
|
const queryRunner = dataSource.createQueryRunner();
|
|
@@ -229,21 +260,26 @@ export class AuthenticationService {
|
|
|
229
260
|
if (!this.isCompanyFeatureEnabled()) {
|
|
230
261
|
return this.generateRegistrationResponse(user);
|
|
231
262
|
}
|
|
232
|
-
return this.processCompanyRegistration(user, dto);
|
|
263
|
+
return await this.processCompanyRegistration(user, dto);
|
|
233
264
|
} catch (error) {
|
|
234
265
|
await queryRunner.rollbackTransaction();
|
|
235
266
|
if (error instanceof BadRequestException) {
|
|
236
267
|
throw error;
|
|
237
268
|
}
|
|
238
|
-
|
|
239
|
-
|
|
269
|
+
throw new BadRequestException({
|
|
270
|
+
message: 'Registration failed. Please try again.',
|
|
271
|
+
messageKey: AUTH_MESSAGES.REGISTER_FAILED
|
|
272
|
+
});
|
|
240
273
|
} finally{
|
|
241
274
|
await queryRunner.release();
|
|
242
275
|
}
|
|
243
276
|
}
|
|
244
277
|
async createUserFromPendingRegistration(pending) {
|
|
245
278
|
if (!this.dataSourceProvider) {
|
|
246
|
-
throw new BadRequestException(
|
|
279
|
+
throw new BadRequestException({
|
|
280
|
+
message: 'Data source not available',
|
|
281
|
+
messageKey: SYSTEM_MESSAGES.DATASOURCE_NOT_AVAILABLE
|
|
282
|
+
});
|
|
247
283
|
}
|
|
248
284
|
const dataSource = await this.dataSourceProvider.getDataSource();
|
|
249
285
|
const queryRunner = dataSource.createQueryRunner();
|
|
@@ -256,7 +292,10 @@ export class AuthenticationService {
|
|
|
256
292
|
}
|
|
257
293
|
});
|
|
258
294
|
if (existingUser) {
|
|
259
|
-
throw new BadRequestException(
|
|
295
|
+
throw new BadRequestException({
|
|
296
|
+
message: 'This email has already been registered.',
|
|
297
|
+
messageKey: AUTH_MESSAGES.REGISTER_EMAIL_EXISTS
|
|
298
|
+
});
|
|
260
299
|
}
|
|
261
300
|
const user = queryRunner.manager.create(User, {
|
|
262
301
|
name: pending.name,
|
|
@@ -273,7 +312,7 @@ export class AuthenticationService {
|
|
|
273
312
|
}
|
|
274
313
|
await queryRunner.commitTransaction();
|
|
275
314
|
if (this.isCompanyFeatureEnabled() && pending.companyData) {
|
|
276
|
-
return this.processCompanyRegistration(user, pending.companyData);
|
|
315
|
+
return await this.processCompanyRegistration(user, pending.companyData);
|
|
277
316
|
}
|
|
278
317
|
return this.generateRegistrationResponse(user);
|
|
279
318
|
} catch (error) {
|
|
@@ -281,8 +320,10 @@ export class AuthenticationService {
|
|
|
281
320
|
if (error instanceof BadRequestException) {
|
|
282
321
|
throw error;
|
|
283
322
|
}
|
|
284
|
-
|
|
285
|
-
|
|
323
|
+
throw new BadRequestException({
|
|
324
|
+
message: 'Registration failed. Please try again.',
|
|
325
|
+
messageKey: AUTH_MESSAGES.REGISTER_FAILED
|
|
326
|
+
});
|
|
286
327
|
} finally{
|
|
287
328
|
await queryRunner.release();
|
|
288
329
|
}
|
|
@@ -292,7 +333,10 @@ export class AuthenticationService {
|
|
|
292
333
|
const branchRepository = await this.getBranchRepository();
|
|
293
334
|
const permissionRepository = await this.getPermissionRepository();
|
|
294
335
|
if (!companyRepository || !branchRepository || !permissionRepository) {
|
|
295
|
-
if (throwOnUnavailable) throw new BadRequestException(
|
|
336
|
+
if (throwOnUnavailable) throw new BadRequestException({
|
|
337
|
+
message: 'Company feature repositories not available',
|
|
338
|
+
messageKey: SYSTEM_MESSAGES.REPOSITORY_NOT_AVAILABLE
|
|
339
|
+
});
|
|
296
340
|
return this.generateRegistrationResponse(user);
|
|
297
341
|
}
|
|
298
342
|
const { company, branch } = await this.findOrCreateCompanyAndBranch(data, companyRepository, branchRepository);
|
|
@@ -310,7 +354,10 @@ export class AuthenticationService {
|
|
|
310
354
|
isActive: true
|
|
311
355
|
}
|
|
312
356
|
});
|
|
313
|
-
if (!company) throw new BadRequestException(
|
|
357
|
+
if (!company) throw new BadRequestException({
|
|
358
|
+
message: 'Company not found or inactive',
|
|
359
|
+
messageKey: AUTH_MESSAGES.COMPANY_NOT_FOUND
|
|
360
|
+
});
|
|
314
361
|
const branch = data.branchSlug ? await branchRepo.findOne({
|
|
315
362
|
where: {
|
|
316
363
|
slug: data.branchSlug,
|
|
@@ -327,7 +374,10 @@ export class AuthenticationService {
|
|
|
327
374
|
createdAt: 'ASC'
|
|
328
375
|
}
|
|
329
376
|
});
|
|
330
|
-
if (data.branchSlug && !branch) throw new BadRequestException(
|
|
377
|
+
if (data.branchSlug && !branch) throw new BadRequestException({
|
|
378
|
+
message: 'Branch not found or inactive',
|
|
379
|
+
messageKey: AUTH_MESSAGES.BRANCH_NOT_FOUND
|
|
380
|
+
});
|
|
331
381
|
return {
|
|
332
382
|
company,
|
|
333
383
|
branch
|
|
@@ -383,17 +433,23 @@ export class AuthenticationService {
|
|
|
383
433
|
async select(sessionId, companyId, branchId) {
|
|
384
434
|
this.ensureCompanyFeatureEnabled();
|
|
385
435
|
if (!this.sessionService) {
|
|
386
|
-
throw new BadRequestException(
|
|
436
|
+
throw new BadRequestException({
|
|
437
|
+
message: 'Session service not available',
|
|
438
|
+
messageKey: AUTH_MESSAGES.SESSION_NOT_AVAILABLE
|
|
439
|
+
});
|
|
387
440
|
}
|
|
388
441
|
const session = await this.sessionService.consumeSession(sessionId);
|
|
389
442
|
if (!session) {
|
|
390
|
-
throw new UnauthorizedException(
|
|
443
|
+
throw new UnauthorizedException({
|
|
444
|
+
message: 'Invalid or expired session',
|
|
445
|
+
messageKey: AUTH_MESSAGES.SESSION_INVALID
|
|
446
|
+
});
|
|
391
447
|
}
|
|
392
|
-
return this.validateAndLoginWithCompany(session.userId, companyId, branchId);
|
|
448
|
+
return await this.validateAndLoginWithCompany(session.userId, companyId, branchId);
|
|
393
449
|
}
|
|
394
450
|
async switchCompany(userId, companyId, branchId) {
|
|
395
451
|
this.ensureCompanyFeatureEnabled();
|
|
396
|
-
return this.validateAndLoginWithCompany(userId, companyId, branchId);
|
|
452
|
+
return await this.validateAndLoginWithCompany(userId, companyId, branchId);
|
|
397
453
|
}
|
|
398
454
|
async validateAndLoginWithCompany(userId, companyId, branchId) {
|
|
399
455
|
const userRepository = await this.getUserRepository();
|
|
@@ -403,13 +459,19 @@ export class AuthenticationService {
|
|
|
403
459
|
}
|
|
404
460
|
});
|
|
405
461
|
if (!user) {
|
|
406
|
-
throw new NotFoundException(
|
|
462
|
+
throw new NotFoundException({
|
|
463
|
+
message: 'User not found',
|
|
464
|
+
messageKey: USER_MESSAGES.NOT_FOUND
|
|
465
|
+
});
|
|
407
466
|
}
|
|
408
467
|
const permissionRepository = await this.getPermissionRepository();
|
|
409
468
|
const companyRepository = await this.getCompanyRepository();
|
|
410
469
|
const branchRepository = await this.getBranchRepository();
|
|
411
470
|
if (!permissionRepository || !companyRepository || !branchRepository) {
|
|
412
|
-
throw new BadRequestException(
|
|
471
|
+
throw new BadRequestException({
|
|
472
|
+
message: 'Repositories not available',
|
|
473
|
+
messageKey: SYSTEM_MESSAGES.REPOSITORY_NOT_AVAILABLE
|
|
474
|
+
});
|
|
413
475
|
}
|
|
414
476
|
const hasCompanyAccess = await permissionRepository.findOne({
|
|
415
477
|
where: {
|
|
@@ -419,7 +481,10 @@ export class AuthenticationService {
|
|
|
419
481
|
}
|
|
420
482
|
});
|
|
421
483
|
if (!hasCompanyAccess) {
|
|
422
|
-
throw new UnauthorizedException(
|
|
484
|
+
throw new UnauthorizedException({
|
|
485
|
+
message: 'No access to this company',
|
|
486
|
+
messageKey: AUTH_MESSAGES.COMPANY_NO_ACCESS
|
|
487
|
+
});
|
|
423
488
|
}
|
|
424
489
|
const company = await companyRepository.findOne({
|
|
425
490
|
where: {
|
|
@@ -428,7 +493,10 @@ export class AuthenticationService {
|
|
|
428
493
|
}
|
|
429
494
|
});
|
|
430
495
|
if (!company) {
|
|
431
|
-
throw new UnauthorizedException(
|
|
496
|
+
throw new UnauthorizedException({
|
|
497
|
+
message: 'Company not found or inactive',
|
|
498
|
+
messageKey: AUTH_MESSAGES.COMPANY_NOT_FOUND
|
|
499
|
+
});
|
|
432
500
|
}
|
|
433
501
|
const hasBranchAccess = await permissionRepository.findOne({
|
|
434
502
|
where: {
|
|
@@ -438,7 +506,10 @@ export class AuthenticationService {
|
|
|
438
506
|
}
|
|
439
507
|
});
|
|
440
508
|
if (!hasBranchAccess) {
|
|
441
|
-
throw new UnauthorizedException(
|
|
509
|
+
throw new UnauthorizedException({
|
|
510
|
+
message: 'No access to this branch',
|
|
511
|
+
messageKey: AUTH_MESSAGES.BRANCH_NO_ACCESS
|
|
512
|
+
});
|
|
442
513
|
}
|
|
443
514
|
const branch = await branchRepository.findOne({
|
|
444
515
|
where: {
|
|
@@ -448,7 +519,10 @@ export class AuthenticationService {
|
|
|
448
519
|
}
|
|
449
520
|
});
|
|
450
521
|
if (!branch) {
|
|
451
|
-
throw new UnauthorizedException(
|
|
522
|
+
throw new UnauthorizedException({
|
|
523
|
+
message: 'Branch not found or inactive',
|
|
524
|
+
messageKey: AUTH_MESSAGES.BRANCH_NOT_FOUND
|
|
525
|
+
});
|
|
452
526
|
}
|
|
453
527
|
return this.generateLoginResponseWithCompany(user, company, branch);
|
|
454
528
|
}
|
|
@@ -458,7 +532,10 @@ export class AuthenticationService {
|
|
|
458
532
|
secret: this.authConfig.getRefreshTokenSecret()
|
|
459
533
|
});
|
|
460
534
|
if (await this.isTokenRevoked(payload.id, payload.iat)) {
|
|
461
|
-
throw new UnauthorizedException(
|
|
535
|
+
throw new UnauthorizedException({
|
|
536
|
+
message: 'Token has been revoked',
|
|
537
|
+
messageKey: AUTH_MESSAGES.TOKEN_REVOKED
|
|
538
|
+
});
|
|
462
539
|
}
|
|
463
540
|
const userRepository = await this.getUserRepository();
|
|
464
541
|
const user = await userRepository.findOne({
|
|
@@ -467,7 +544,10 @@ export class AuthenticationService {
|
|
|
467
544
|
}
|
|
468
545
|
});
|
|
469
546
|
if (!user || !user.isActive) {
|
|
470
|
-
throw new UnauthorizedException(
|
|
547
|
+
throw new UnauthorizedException({
|
|
548
|
+
message: 'Invalid refresh token',
|
|
549
|
+
messageKey: AUTH_MESSAGES.TOKEN_INVALID
|
|
550
|
+
});
|
|
471
551
|
}
|
|
472
552
|
return this.generateTokens({
|
|
473
553
|
id: user.id,
|
|
@@ -481,9 +561,11 @@ export class AuthenticationService {
|
|
|
481
561
|
branchId: payload.branchId
|
|
482
562
|
}
|
|
483
563
|
});
|
|
484
|
-
} catch
|
|
485
|
-
|
|
486
|
-
|
|
564
|
+
} catch {
|
|
565
|
+
throw new UnauthorizedException({
|
|
566
|
+
message: 'Invalid or expired refresh token',
|
|
567
|
+
messageKey: AUTH_MESSAGES.TOKEN_EXPIRED
|
|
568
|
+
});
|
|
487
569
|
}
|
|
488
570
|
}
|
|
489
571
|
async changePassword(userId, currentPassword, newPassword) {
|
|
@@ -493,24 +575,31 @@ export class AuthenticationService {
|
|
|
493
575
|
id: userId
|
|
494
576
|
}
|
|
495
577
|
});
|
|
496
|
-
if (!user) throw new UnauthorizedException(
|
|
578
|
+
if (!user) throw new UnauthorizedException({
|
|
579
|
+
message: 'User not found',
|
|
580
|
+
messageKey: USER_MESSAGES.NOT_FOUND
|
|
581
|
+
});
|
|
497
582
|
if (!await bcrypt.compare(currentPassword, user.password || '')) {
|
|
498
|
-
throw new UnauthorizedException(
|
|
583
|
+
throw new UnauthorizedException({
|
|
584
|
+
message: 'Current password is incorrect',
|
|
585
|
+
messageKey: AUTH_MESSAGES.PASSWORD_CHANGE_INVALID_CURRENT
|
|
586
|
+
});
|
|
499
587
|
}
|
|
500
588
|
user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
|
|
501
589
|
await userRepository.save(user);
|
|
502
590
|
}
|
|
503
591
|
async logout(userId) {
|
|
504
592
|
if (!this.cacheManager) {
|
|
505
|
-
|
|
506
|
-
|
|
593
|
+
throw new BadRequestException({
|
|
594
|
+
message: 'Logout service temporarily unavailable. Please try again later.',
|
|
595
|
+
messageKey: AUTH_MESSAGES.LOGOUT_SERVICE_UNAVAILABLE
|
|
596
|
+
});
|
|
507
597
|
}
|
|
508
598
|
const revokedAt = Math.floor(Date.now() / 1000);
|
|
509
599
|
const cacheKey = `${TOKEN_REVOKED_PREFIX}:${userId}`;
|
|
510
600
|
const refreshTokenExpiry = this.authConfig.getRefreshTokenExpiration();
|
|
511
601
|
const ttlMs = this.parseExpiryToMs(refreshTokenExpiry);
|
|
512
602
|
await this.cacheManager.set(cacheKey, revokedAt, ttlMs);
|
|
513
|
-
this.logger.log(`User ${userId} logged out - all tokens revoked`);
|
|
514
603
|
}
|
|
515
604
|
async isTokenRevoked(userId, tokenIssuedAt) {
|
|
516
605
|
if (!this.cacheManager || !tokenIssuedAt) {
|
|
@@ -546,7 +635,10 @@ export class AuthenticationService {
|
|
|
546
635
|
const permissionRepository = await this.getPermissionRepository();
|
|
547
636
|
const companyRepository = await this.getCompanyRepository();
|
|
548
637
|
if (!permissionRepository || !companyRepository) {
|
|
549
|
-
throw new BadRequestException(
|
|
638
|
+
throw new BadRequestException({
|
|
639
|
+
message: 'Repositories not available',
|
|
640
|
+
messageKey: SYSTEM_MESSAGES.REPOSITORY_NOT_AVAILABLE
|
|
641
|
+
});
|
|
550
642
|
}
|
|
551
643
|
const permissions = await permissionRepository.find({
|
|
552
644
|
where: {
|
|
@@ -567,7 +659,10 @@ export class AuthenticationService {
|
|
|
567
659
|
const permissionRepository = await this.getPermissionRepository();
|
|
568
660
|
const branchRepository = await this.getBranchRepository();
|
|
569
661
|
if (!permissionRepository || !branchRepository) {
|
|
570
|
-
throw new BadRequestException(
|
|
662
|
+
throw new BadRequestException({
|
|
663
|
+
message: 'Repositories not available',
|
|
664
|
+
messageKey: SYSTEM_MESSAGES.REPOSITORY_NOT_AVAILABLE
|
|
665
|
+
});
|
|
571
666
|
}
|
|
572
667
|
const companyPermission = await permissionRepository.findOne({
|
|
573
668
|
where: {
|
|
@@ -577,7 +672,10 @@ export class AuthenticationService {
|
|
|
577
672
|
}
|
|
578
673
|
});
|
|
579
674
|
if (!companyPermission) {
|
|
580
|
-
throw new UnauthorizedException(
|
|
675
|
+
throw new UnauthorizedException({
|
|
676
|
+
message: 'No access to this company',
|
|
677
|
+
messageKey: AUTH_MESSAGES.COMPANY_NO_ACCESS
|
|
678
|
+
});
|
|
581
679
|
}
|
|
582
680
|
const branchPermissions = await permissionRepository.find({
|
|
583
681
|
where: {
|
|
@@ -624,20 +722,32 @@ export class AuthenticationService {
|
|
|
624
722
|
id: userId
|
|
625
723
|
}
|
|
626
724
|
});
|
|
627
|
-
if (!user) throw new UnauthorizedException(
|
|
725
|
+
if (!user) throw new UnauthorizedException({
|
|
726
|
+
message: 'User not found',
|
|
727
|
+
messageKey: USER_MESSAGES.NOT_FOUND
|
|
728
|
+
});
|
|
628
729
|
const response = {
|
|
629
730
|
user: this.toUserInfo(user)
|
|
630
731
|
};
|
|
631
732
|
if (this.isCompanyFeatureEnabled()) {
|
|
632
733
|
if (!companyId) {
|
|
633
|
-
throw new UnauthorizedException(
|
|
734
|
+
throw new UnauthorizedException({
|
|
735
|
+
message: 'Company feature is enabled but user has no company assigned.',
|
|
736
|
+
messageKey: AUTH_MESSAGES.COMPANY_REQUIRED
|
|
737
|
+
});
|
|
634
738
|
}
|
|
635
739
|
const company = await this.getCompanyById(companyId);
|
|
636
|
-
if (!company) throw new UnauthorizedException(
|
|
740
|
+
if (!company) throw new UnauthorizedException({
|
|
741
|
+
message: 'Company not found.',
|
|
742
|
+
messageKey: AUTH_MESSAGES.COMPANY_NOT_FOUND
|
|
743
|
+
});
|
|
637
744
|
response.company = this.toCompanyInfo(company);
|
|
638
745
|
if (branchId) {
|
|
639
746
|
const branch = await this.getBranchById(branchId);
|
|
640
|
-
if (!branch) throw new UnauthorizedException(
|
|
747
|
+
if (!branch) throw new UnauthorizedException({
|
|
748
|
+
message: 'Branch not found.',
|
|
749
|
+
messageKey: AUTH_MESSAGES.BRANCH_NOT_FOUND
|
|
750
|
+
});
|
|
641
751
|
response.branch = this.toBranchInfo(branch);
|
|
642
752
|
}
|
|
643
753
|
}
|
|
@@ -645,7 +755,10 @@ export class AuthenticationService {
|
|
|
645
755
|
}
|
|
646
756
|
ensureCompanyFeatureEnabled() {
|
|
647
757
|
if (!this.isCompanyFeatureEnabled()) {
|
|
648
|
-
throw new BadRequestException(
|
|
758
|
+
throw new BadRequestException({
|
|
759
|
+
message: 'Company feature is not enabled',
|
|
760
|
+
messageKey: AUTH_MESSAGES.COMPANY_FEATURE_NOT_ENABLED
|
|
761
|
+
});
|
|
649
762
|
}
|
|
650
763
|
}
|
|
651
764
|
async validateUser(email, password) {
|
|
@@ -656,7 +769,10 @@ export class AuthenticationService {
|
|
|
656
769
|
}
|
|
657
770
|
});
|
|
658
771
|
if (!user || !await bcrypt.compare(password, user.password || '')) {
|
|
659
|
-
throw new UnauthorizedException(
|
|
772
|
+
throw new UnauthorizedException({
|
|
773
|
+
message: 'Invalid credentials',
|
|
774
|
+
messageKey: AUTH_MESSAGES.LOGIN_INVALID_CREDENTIALS
|
|
775
|
+
});
|
|
660
776
|
}
|
|
661
777
|
user.lastLoginAt = new Date();
|
|
662
778
|
await userRepository.save(user);
|
|
@@ -713,6 +829,7 @@ export class AuthenticationService {
|
|
|
713
829
|
email: user.email,
|
|
714
830
|
type: 'access'
|
|
715
831
|
});
|
|
832
|
+
this.sendLoginNotification(user.id);
|
|
716
833
|
return {
|
|
717
834
|
accessToken,
|
|
718
835
|
refreshToken,
|
|
@@ -727,6 +844,7 @@ export class AuthenticationService {
|
|
|
727
844
|
companyId: company.id,
|
|
728
845
|
branchId: branch?.id
|
|
729
846
|
});
|
|
847
|
+
this.sendLoginNotification(user.id, company.id);
|
|
730
848
|
return {
|
|
731
849
|
accessToken,
|
|
732
850
|
refreshToken,
|
|
@@ -735,9 +853,24 @@ export class AuthenticationService {
|
|
|
735
853
|
branch: branch ? this.toBranchInfo(branch) : undefined
|
|
736
854
|
};
|
|
737
855
|
}
|
|
856
|
+
sendLoginNotification(userId, companyId) {
|
|
857
|
+
if (!this.notificationAdapter) return;
|
|
858
|
+
this.notificationAdapter.send({
|
|
859
|
+
userId,
|
|
860
|
+
title: 'Login successful',
|
|
861
|
+
message: 'You have successfully logged in to your account.',
|
|
862
|
+
type: NotificationType.INFO,
|
|
863
|
+
companyId
|
|
864
|
+
}).catch(()=>{
|
|
865
|
+
// Silent failure - notification is non-critical
|
|
866
|
+
});
|
|
867
|
+
}
|
|
738
868
|
async generateCompanySelectionResponse(user, companies, branches) {
|
|
739
869
|
if (!this.sessionService) {
|
|
740
|
-
throw new BadRequestException(
|
|
870
|
+
throw new BadRequestException({
|
|
871
|
+
message: 'Session service not available',
|
|
872
|
+
messageKey: AUTH_MESSAGES.SESSION_NOT_AVAILABLE
|
|
873
|
+
});
|
|
741
874
|
}
|
|
742
875
|
const sessionId = await this.sessionService.createSession(user.id);
|
|
743
876
|
const companiesWithBranches = companies.map((company)=>({
|
|
@@ -769,7 +902,7 @@ export class AuthenticationService {
|
|
|
769
902
|
};
|
|
770
903
|
}
|
|
771
904
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
772
|
-
constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService, userEnricher){
|
|
905
|
+
constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService, userEnricher, notificationAdapter){
|
|
773
906
|
_define_property(this, "jwtService", void 0);
|
|
774
907
|
_define_property(this, "authConfig", void 0);
|
|
775
908
|
_define_property(this, "cacheManager", void 0);
|
|
@@ -777,7 +910,7 @@ export class AuthenticationService {
|
|
|
777
910
|
_define_property(this, "sessionService", void 0);
|
|
778
911
|
_define_property(this, "authEmailService", void 0);
|
|
779
912
|
_define_property(this, "userEnricher", void 0);
|
|
780
|
-
_define_property(this, "
|
|
913
|
+
_define_property(this, "notificationAdapter", void 0);
|
|
781
914
|
this.jwtService = jwtService;
|
|
782
915
|
this.authConfig = authConfig;
|
|
783
916
|
this.cacheManager = cacheManager;
|
|
@@ -785,9 +918,109 @@ export class AuthenticationService {
|
|
|
785
918
|
this.sessionService = sessionService;
|
|
786
919
|
this.authEmailService = authEmailService;
|
|
787
920
|
this.userEnricher = userEnricher;
|
|
788
|
-
this.
|
|
921
|
+
this.notificationAdapter = notificationAdapter;
|
|
789
922
|
}
|
|
790
923
|
}
|
|
924
|
+
_ts_decorate([
|
|
925
|
+
LogAction({
|
|
926
|
+
action: 'auth.login',
|
|
927
|
+
module: 'auth',
|
|
928
|
+
includeParams: false
|
|
929
|
+
}),
|
|
930
|
+
_ts_metadata("design:type", Function),
|
|
931
|
+
_ts_metadata("design:paramtypes", [
|
|
932
|
+
String,
|
|
933
|
+
String
|
|
934
|
+
]),
|
|
935
|
+
_ts_metadata("design:returntype", Promise)
|
|
936
|
+
], AuthenticationService.prototype, "login", null);
|
|
937
|
+
_ts_decorate([
|
|
938
|
+
LogAction({
|
|
939
|
+
action: 'auth.register',
|
|
940
|
+
module: 'auth',
|
|
941
|
+
includeParams: false
|
|
942
|
+
}),
|
|
943
|
+
_ts_metadata("design:type", Function),
|
|
944
|
+
_ts_metadata("design:paramtypes", [
|
|
945
|
+
typeof RegistrationDto === "undefined" ? Object : RegistrationDto,
|
|
946
|
+
String
|
|
947
|
+
]),
|
|
948
|
+
_ts_metadata("design:returntype", Promise)
|
|
949
|
+
], AuthenticationService.prototype, "register", null);
|
|
950
|
+
_ts_decorate([
|
|
951
|
+
LogAction({
|
|
952
|
+
action: 'auth.createUser',
|
|
953
|
+
module: 'auth',
|
|
954
|
+
includeParams: false
|
|
955
|
+
}),
|
|
956
|
+
_ts_metadata("design:type", Function),
|
|
957
|
+
_ts_metadata("design:paramtypes", [
|
|
958
|
+
typeof RegistrationDto === "undefined" ? Object : RegistrationDto
|
|
959
|
+
]),
|
|
960
|
+
_ts_metadata("design:returntype", Promise)
|
|
961
|
+
], AuthenticationService.prototype, "createUserDirectly", null);
|
|
962
|
+
_ts_decorate([
|
|
963
|
+
LogAction({
|
|
964
|
+
action: 'auth.createUserFromPending',
|
|
965
|
+
module: 'auth',
|
|
966
|
+
includeParams: false
|
|
967
|
+
}),
|
|
968
|
+
_ts_metadata("design:type", Function),
|
|
969
|
+
_ts_metadata("design:paramtypes", [
|
|
970
|
+
typeof PendingRegistration === "undefined" ? Object : PendingRegistration
|
|
971
|
+
]),
|
|
972
|
+
_ts_metadata("design:returntype", Promise)
|
|
973
|
+
], AuthenticationService.prototype, "createUserFromPendingRegistration", null);
|
|
974
|
+
_ts_decorate([
|
|
975
|
+
LogAction({
|
|
976
|
+
action: 'auth.companySwitch',
|
|
977
|
+
module: 'auth'
|
|
978
|
+
}),
|
|
979
|
+
_ts_metadata("design:type", Function),
|
|
980
|
+
_ts_metadata("design:paramtypes", [
|
|
981
|
+
String,
|
|
982
|
+
String,
|
|
983
|
+
String
|
|
984
|
+
]),
|
|
985
|
+
_ts_metadata("design:returntype", Promise)
|
|
986
|
+
], AuthenticationService.prototype, "validateAndLoginWithCompany", null);
|
|
987
|
+
_ts_decorate([
|
|
988
|
+
LogAction({
|
|
989
|
+
action: 'auth.refreshToken',
|
|
990
|
+
module: 'auth',
|
|
991
|
+
includeParams: false
|
|
992
|
+
}),
|
|
993
|
+
_ts_metadata("design:type", Function),
|
|
994
|
+
_ts_metadata("design:paramtypes", [
|
|
995
|
+
String
|
|
996
|
+
]),
|
|
997
|
+
_ts_metadata("design:returntype", Promise)
|
|
998
|
+
], AuthenticationService.prototype, "refreshToken", null);
|
|
999
|
+
_ts_decorate([
|
|
1000
|
+
LogAction({
|
|
1001
|
+
action: 'auth.changePassword',
|
|
1002
|
+
module: 'auth',
|
|
1003
|
+
includeParams: false
|
|
1004
|
+
}),
|
|
1005
|
+
_ts_metadata("design:type", Function),
|
|
1006
|
+
_ts_metadata("design:paramtypes", [
|
|
1007
|
+
String,
|
|
1008
|
+
String,
|
|
1009
|
+
String
|
|
1010
|
+
]),
|
|
1011
|
+
_ts_metadata("design:returntype", Promise)
|
|
1012
|
+
], AuthenticationService.prototype, "changePassword", null);
|
|
1013
|
+
_ts_decorate([
|
|
1014
|
+
LogAction({
|
|
1015
|
+
action: 'auth.logout',
|
|
1016
|
+
module: 'auth'
|
|
1017
|
+
}),
|
|
1018
|
+
_ts_metadata("design:type", Function),
|
|
1019
|
+
_ts_metadata("design:paramtypes", [
|
|
1020
|
+
String
|
|
1021
|
+
]),
|
|
1022
|
+
_ts_metadata("design:returntype", Promise)
|
|
1023
|
+
], AuthenticationService.prototype, "logout", null);
|
|
791
1024
|
AuthenticationService = _ts_decorate([
|
|
792
1025
|
Injectable({
|
|
793
1026
|
scope: Scope.REQUEST
|
|
@@ -804,6 +1037,8 @@ AuthenticationService = _ts_decorate([
|
|
|
804
1037
|
_ts_param(5, Inject(AuthEmailService)),
|
|
805
1038
|
_ts_param(6, Optional()),
|
|
806
1039
|
_ts_param(6, Inject(USER_ENRICHER)),
|
|
1040
|
+
_ts_param(7, Optional()),
|
|
1041
|
+
_ts_param(7, Inject(NOTIFICATION_ADAPTER)),
|
|
807
1042
|
_ts_metadata("design:type", Function),
|
|
808
1043
|
_ts_metadata("design:paramtypes", [
|
|
809
1044
|
typeof JwtService === "undefined" ? Object : JwtService,
|
|
@@ -812,6 +1047,7 @@ AuthenticationService = _ts_decorate([
|
|
|
812
1047
|
typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
|
|
813
1048
|
typeof CompanySelectionSessionService === "undefined" ? Object : CompanySelectionSessionService,
|
|
814
1049
|
typeof AuthEmailService === "undefined" ? Object : AuthEmailService,
|
|
815
|
-
typeof IUserEnricher === "undefined" ? Object : IUserEnricher
|
|
1050
|
+
typeof IUserEnricher === "undefined" ? Object : IUserEnricher,
|
|
1051
|
+
typeof INotificationAdapter === "undefined" ? Object : INotificationAdapter
|
|
816
1052
|
])
|
|
817
1053
|
], AuthenticationService);
|