@flusys/nestjs-auth 3.0.0-rc → 3.0.1
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 +610 -95
- package/cjs/controllers/authentication.controller.js +2 -173
- package/cjs/controllers/email-verification.controller.js +220 -0
- package/cjs/controllers/index.js +1 -0
- package/cjs/dtos/authentication.dto.js +12 -2
- package/cjs/entities/index.js +16 -17
- package/cjs/entities/pending-registration.entity.js +9 -0
- package/cjs/modules/auth.module.js +33 -24
- package/cjs/services/auth-config.service.js +11 -7
- package/cjs/services/auth-datasource.provider.js +15 -27
- package/cjs/services/auth-email.service.js +8 -33
- package/cjs/services/authentication.service.js +155 -110
- package/cjs/services/user.service.js +28 -83
- package/controllers/authentication.controller.d.ts +2 -10
- package/controllers/email-verification.controller.d.ts +20 -0
- package/controllers/index.d.ts +1 -0
- package/dtos/authentication.dto.d.ts +1 -0
- package/entities/index.d.ts +5 -10
- package/entities/pending-registration.entity.d.ts +1 -0
- package/fesm/controllers/authentication.controller.js +3 -174
- package/fesm/controllers/email-verification.controller.js +210 -0
- package/fesm/controllers/index.js +1 -0
- package/fesm/dtos/authentication.dto.js +32 -25
- package/fesm/entities/index.js +17 -9
- package/fesm/entities/pending-registration.entity.js +9 -0
- package/fesm/interfaces/user-enricher.interface.js +2 -24
- package/fesm/modules/auth.module.js +35 -26
- package/fesm/services/auth-config.service.js +11 -7
- package/fesm/services/auth-datasource.provider.js +15 -27
- package/fesm/services/auth-email.service.js +8 -33
- package/fesm/services/authentication.service.js +145 -100
- package/fesm/services/user.service.js +28 -83
- package/interfaces/user-enricher.interface.d.ts +3 -2
- package/modules/auth.module.d.ts +3 -1
- package/package.json +3 -3
- package/services/auth-config.service.d.ts +1 -0
- package/services/auth-datasource.provider.d.ts +1 -0
- package/services/auth-email.service.d.ts +1 -0
- package/services/authentication.service.d.ts +7 -1
- package/services/user.service.d.ts +5 -0
|
@@ -26,11 +26,14 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
|
+
import { INotificationAdapter, NOTIFICATION_ADAPTER } from '@flusys/nestjs-shared/interfaces';
|
|
30
|
+
import { NotificationType } from '@flusys/nestjs-shared/enums';
|
|
29
31
|
import { ErrorHandler, generateUniqueSlug } from '@flusys/nestjs-shared/utils';
|
|
30
32
|
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Optional, Scope, UnauthorizedException } from '@nestjs/common';
|
|
31
33
|
import { JwtService } from '@nestjs/jwt';
|
|
32
34
|
import * as bcrypt from 'bcrypt';
|
|
33
|
-
import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX } from '../config';
|
|
35
|
+
import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX, USER_ENRICHER } from '../config';
|
|
36
|
+
import { IUserEnricher } from '../interfaces/user-enricher.interface';
|
|
34
37
|
import { CompanyBranch } from '../entities/company-branch.entity';
|
|
35
38
|
import { Company } from '../entities/company.entity';
|
|
36
39
|
import { UserCompanyPermission } from '../entities/user-company-permission.entity';
|
|
@@ -41,11 +44,14 @@ import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
|
41
44
|
import { AuthEmailService } from './auth-email.service';
|
|
42
45
|
import { CompanySelectionSessionService } from './company-selection-session.service';
|
|
43
46
|
export class AuthenticationService {
|
|
44
|
-
|
|
45
|
-
/** Check if company feature is enabled (tenant-aware with global fallback) */ isCompanyFeatureEnabled() {
|
|
47
|
+
isCompanyFeatureEnabled() {
|
|
46
48
|
return this.dataSourceProvider?.isCompanyFeatureEnabled() ?? this.authConfig.isCompanyFeatureEnabled();
|
|
47
49
|
}
|
|
48
|
-
|
|
50
|
+
isEmailVerificationEnabled() {
|
|
51
|
+
const featureEnabled = this.dataSourceProvider?.isEmailVerificationEnabled() ?? this.authConfig.isEmailVerificationEnabled();
|
|
52
|
+
const providerAvailable = this.authEmailService?.isEmailEnabled() ?? false;
|
|
53
|
+
return featureEnabled && providerAvailable;
|
|
54
|
+
}
|
|
49
55
|
async getUserRepository() {
|
|
50
56
|
if (!this.dataSourceProvider) {
|
|
51
57
|
throw new Error('User repository not available');
|
|
@@ -64,10 +70,8 @@ export class AuthenticationService {
|
|
|
64
70
|
if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
|
|
65
71
|
return this.dataSourceProvider.getRepository(CompanyBranch);
|
|
66
72
|
}
|
|
67
|
-
// Login
|
|
68
73
|
async login(email, password) {
|
|
69
|
-
|
|
70
|
-
if (await this.authEmailService?.hasPendingRegistration(email)) {
|
|
74
|
+
if (this.isEmailVerificationEnabled() && await this.authEmailService.hasPendingRegistration(email)) {
|
|
71
75
|
return {
|
|
72
76
|
requiresEmailVerification: true,
|
|
73
77
|
email,
|
|
@@ -78,7 +82,7 @@ export class AuthenticationService {
|
|
|
78
82
|
if (!user.isActive) {
|
|
79
83
|
throw new UnauthorizedException('User account is deactivated');
|
|
80
84
|
}
|
|
81
|
-
if (this.
|
|
85
|
+
if (this.isEmailVerificationEnabled() && !user.emailVerified) {
|
|
82
86
|
return {
|
|
83
87
|
requiresEmailVerification: true,
|
|
84
88
|
email: user.email,
|
|
@@ -90,20 +94,18 @@ export class AuthenticationService {
|
|
|
90
94
|
}
|
|
91
95
|
return this.handleCompanyBasedLogin(user);
|
|
92
96
|
}
|
|
93
|
-
|
|
97
|
+
async handleCompanyBasedLogin(user) {
|
|
94
98
|
const permissionRepository = await this.getPermissionRepository();
|
|
95
99
|
const branchRepository = await this.getBranchRepository();
|
|
96
100
|
if (!permissionRepository || !branchRepository) {
|
|
97
101
|
return this.generateLoginResponse(user);
|
|
98
102
|
}
|
|
99
|
-
// Get company permissions
|
|
100
103
|
const companyPermissions = await permissionRepository.find({
|
|
101
104
|
where: {
|
|
102
105
|
userId: user.id,
|
|
103
106
|
permissionType: PermissionType.COMPANY
|
|
104
107
|
}
|
|
105
108
|
});
|
|
106
|
-
// Load companies
|
|
107
109
|
const companyRepository = await this.getCompanyRepository();
|
|
108
110
|
if (!companyRepository) {
|
|
109
111
|
return this.generateLoginResponse(user);
|
|
@@ -114,12 +116,9 @@ export class AuthenticationService {
|
|
|
114
116
|
}).andWhere('c.is_active = :active', {
|
|
115
117
|
active: true
|
|
116
118
|
}).getMany() : [];
|
|
117
|
-
// If user has no company assignments yet, return simple login response
|
|
118
|
-
// This allows users to login and be assigned to companies later
|
|
119
119
|
if (companies.length === 0) {
|
|
120
120
|
return this.generateLoginResponse(user);
|
|
121
121
|
}
|
|
122
|
-
// Get branch permissions
|
|
123
122
|
const branchPermissions = await permissionRepository.find({
|
|
124
123
|
where: {
|
|
125
124
|
userId: user.id,
|
|
@@ -132,32 +131,25 @@ export class AuthenticationService {
|
|
|
132
131
|
}).andWhere('b.is_active = :active', {
|
|
133
132
|
active: true
|
|
134
133
|
}).getMany() : [];
|
|
135
|
-
// Case 1: Single company, single branch → Direct login
|
|
136
134
|
if (companies.length === 1 && branches.length === 1) {
|
|
137
135
|
return this.generateLoginResponseWithCompany(user, companies[0], branches[0]);
|
|
138
136
|
}
|
|
139
|
-
// Case 2: Multiple options → Selection required (frontend handles UI)
|
|
140
|
-
// Returns all companies with their branches for frontend to display
|
|
141
137
|
return await this.generateCompanySelectionResponse(user, companies, branches);
|
|
142
138
|
}
|
|
143
|
-
|
|
144
|
-
/** Register a new user. If email enabled, creates pending registration. Otherwise creates user directly. */ async register(dto, verifyUrlBase) {
|
|
139
|
+
async register(dto, verifyUrlBase) {
|
|
145
140
|
const userRepository = await this.getUserRepository();
|
|
146
141
|
const existingUser = await userRepository.findOne({
|
|
147
142
|
where: {
|
|
148
143
|
email: dto.email
|
|
149
144
|
}
|
|
150
145
|
});
|
|
151
|
-
// When company feature is enabled and user exists, allow joining/creating company
|
|
152
146
|
if (existingUser && this.isCompanyFeatureEnabled()) {
|
|
153
147
|
return this.handleExistingUserCompanyRegistration(existingUser, dto);
|
|
154
148
|
}
|
|
155
|
-
// User exists but company feature is off - email must be unique
|
|
156
149
|
if (existingUser) {
|
|
157
150
|
throw new BadRequestException('Registration failed due to email exists. Please try again or contact support.');
|
|
158
151
|
}
|
|
159
|
-
|
|
160
|
-
if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
|
|
152
|
+
if (this.isEmailVerificationEnabled() && verifyUrlBase) {
|
|
161
153
|
const companyData = this.isCompanyFeatureEnabled() ? {
|
|
162
154
|
companySlug: dto.companySlug,
|
|
163
155
|
branchSlug: dto.branchSlug,
|
|
@@ -171,6 +163,7 @@ export class AuthenticationService {
|
|
|
171
163
|
phone: dto.phone,
|
|
172
164
|
password: dto.password,
|
|
173
165
|
companyData,
|
|
166
|
+
additionalFields: dto.additionalFields,
|
|
174
167
|
verifyUrlBase
|
|
175
168
|
});
|
|
176
169
|
return {
|
|
@@ -178,10 +171,9 @@ export class AuthenticationService {
|
|
|
178
171
|
message: result.message
|
|
179
172
|
};
|
|
180
173
|
}
|
|
181
|
-
// No email verification - create user directly
|
|
182
174
|
return this.createUserDirectly(dto);
|
|
183
175
|
}
|
|
184
|
-
|
|
176
|
+
async handleExistingUserCompanyRegistration(existingUser, dto) {
|
|
185
177
|
if (!dto.companySlug && !dto.newCompanyName) {
|
|
186
178
|
throw new BadRequestException('Email already registered. Please login or provide a company to join/create.');
|
|
187
179
|
}
|
|
@@ -190,7 +182,6 @@ export class AuthenticationService {
|
|
|
190
182
|
if (!permissionRepository || !companyRepository) {
|
|
191
183
|
throw new BadRequestException('Company feature repositories not available');
|
|
192
184
|
}
|
|
193
|
-
// For existing users joining a company, check if already assigned
|
|
194
185
|
if (dto.companySlug) {
|
|
195
186
|
const company = await companyRepository.findOne({
|
|
196
187
|
where: {
|
|
@@ -213,50 +204,92 @@ export class AuthenticationService {
|
|
|
213
204
|
}
|
|
214
205
|
return this.processCompanyRegistration(existingUser, dto, true);
|
|
215
206
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
207
|
+
async createUserDirectly(dto) {
|
|
208
|
+
if (!this.dataSourceProvider) {
|
|
209
|
+
throw new BadRequestException('Data source not available');
|
|
210
|
+
}
|
|
211
|
+
const dataSource = await this.dataSourceProvider.getDataSource();
|
|
212
|
+
const queryRunner = dataSource.createQueryRunner();
|
|
213
|
+
await queryRunner.connect();
|
|
214
|
+
await queryRunner.startTransaction();
|
|
215
|
+
try {
|
|
216
|
+
const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
217
|
+
const user = queryRunner.manager.create(User, {
|
|
218
|
+
name: dto.name,
|
|
219
|
+
email: dto.email,
|
|
220
|
+
phone: dto.phone,
|
|
221
|
+
password: hashedPassword,
|
|
222
|
+
isActive: true,
|
|
223
|
+
emailVerified: false,
|
|
224
|
+
phoneVerified: false
|
|
225
|
+
});
|
|
226
|
+
await queryRunner.manager.save(User, user);
|
|
227
|
+
if (this.userEnricher?.onUserCreated) {
|
|
228
|
+
await this.userEnricher.onUserCreated(user.id, dto.additionalFields ?? null, queryRunner);
|
|
229
|
+
}
|
|
230
|
+
await queryRunner.commitTransaction();
|
|
231
|
+
if (!this.isCompanyFeatureEnabled()) {
|
|
232
|
+
return this.generateRegistrationResponse(user);
|
|
233
|
+
}
|
|
234
|
+
return this.processCompanyRegistration(user, dto);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
await queryRunner.rollbackTransaction();
|
|
237
|
+
if (error instanceof BadRequestException) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
ErrorHandler.logError(this.logger, error, 'createUserDirectly');
|
|
241
|
+
throw new BadRequestException('Registration failed. Please try again.');
|
|
242
|
+
} finally{
|
|
243
|
+
await queryRunner.release();
|
|
231
244
|
}
|
|
232
|
-
return this.processCompanyRegistration(user, dto);
|
|
233
245
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
246
|
+
async createUserFromPendingRegistration(pending) {
|
|
247
|
+
if (!this.dataSourceProvider) {
|
|
248
|
+
throw new BadRequestException('Data source not available');
|
|
249
|
+
}
|
|
250
|
+
const dataSource = await this.dataSourceProvider.getDataSource();
|
|
251
|
+
const queryRunner = dataSource.createQueryRunner();
|
|
252
|
+
await queryRunner.connect();
|
|
253
|
+
await queryRunner.startTransaction();
|
|
254
|
+
try {
|
|
255
|
+
const existingUser = await queryRunner.manager.findOne(User, {
|
|
256
|
+
where: {
|
|
257
|
+
email: pending.email
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
if (existingUser) {
|
|
261
|
+
throw new BadRequestException('This email has already been registered.');
|
|
239
262
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
const user = queryRunner.manager.create(User, {
|
|
264
|
+
name: pending.name,
|
|
265
|
+
email: pending.email,
|
|
266
|
+
phone: pending.phone,
|
|
267
|
+
password: pending.passwordHash,
|
|
268
|
+
isActive: true,
|
|
269
|
+
emailVerified: true,
|
|
270
|
+
phoneVerified: false
|
|
271
|
+
});
|
|
272
|
+
await queryRunner.manager.save(User, user);
|
|
273
|
+
if (this.userEnricher?.onUserCreated) {
|
|
274
|
+
await this.userEnricher.onUserCreated(user.id, pending.additionalFields ?? null, queryRunner);
|
|
275
|
+
}
|
|
276
|
+
await queryRunner.commitTransaction();
|
|
277
|
+
if (this.isCompanyFeatureEnabled() && pending.companyData) {
|
|
278
|
+
return this.processCompanyRegistration(user, pending.companyData);
|
|
279
|
+
}
|
|
280
|
+
return this.generateRegistrationResponse(user);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
await queryRunner.rollbackTransaction();
|
|
283
|
+
if (error instanceof BadRequestException) {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
ErrorHandler.logError(this.logger, error, 'createUserFromPendingRegistration');
|
|
287
|
+
throw new BadRequestException('Registration failed. Please try again.');
|
|
288
|
+
} finally{
|
|
289
|
+
await queryRunner.release();
|
|
256
290
|
}
|
|
257
|
-
return this.generateRegistrationResponse(user);
|
|
258
291
|
}
|
|
259
|
-
|
|
292
|
+
async processCompanyRegistration(user, data, throwOnUnavailable = false) {
|
|
260
293
|
const companyRepository = await this.getCompanyRepository();
|
|
261
294
|
const branchRepository = await this.getBranchRepository();
|
|
262
295
|
const permissionRepository = await this.getPermissionRepository();
|
|
@@ -271,7 +304,7 @@ export class AuthenticationService {
|
|
|
271
304
|
}
|
|
272
305
|
return this.generateRegistrationResponse(user);
|
|
273
306
|
}
|
|
274
|
-
|
|
307
|
+
async findOrCreateCompanyAndBranch(data, companyRepo, branchRepo) {
|
|
275
308
|
if (data.companySlug) {
|
|
276
309
|
const company = await companyRepo.findOne({
|
|
277
310
|
where: {
|
|
@@ -335,7 +368,7 @@ export class AuthenticationService {
|
|
|
335
368
|
branch: null
|
|
336
369
|
};
|
|
337
370
|
}
|
|
338
|
-
|
|
371
|
+
async assignUserPermissions(userId, companyId, branchId, permissionRepo) {
|
|
339
372
|
await permissionRepo.save(permissionRepo.create({
|
|
340
373
|
userId,
|
|
341
374
|
permissionType: PermissionType.COMPANY,
|
|
@@ -349,8 +382,7 @@ export class AuthenticationService {
|
|
|
349
382
|
}));
|
|
350
383
|
}
|
|
351
384
|
}
|
|
352
|
-
|
|
353
|
-
/** Select company and branch (step 2 of login). Frontend receives all options and user selects both. */ async select(sessionId, companyId, branchId) {
|
|
385
|
+
async select(sessionId, companyId, branchId) {
|
|
354
386
|
this.ensureCompanyFeatureEnabled();
|
|
355
387
|
if (!this.sessionService) {
|
|
356
388
|
throw new BadRequestException('Session service not available');
|
|
@@ -361,11 +393,11 @@ export class AuthenticationService {
|
|
|
361
393
|
}
|
|
362
394
|
return this.validateAndLoginWithCompany(session.userId, companyId, branchId);
|
|
363
395
|
}
|
|
364
|
-
|
|
396
|
+
async switchCompany(userId, companyId, branchId) {
|
|
365
397
|
this.ensureCompanyFeatureEnabled();
|
|
366
398
|
return this.validateAndLoginWithCompany(userId, companyId, branchId);
|
|
367
399
|
}
|
|
368
|
-
|
|
400
|
+
async validateAndLoginWithCompany(userId, companyId, branchId) {
|
|
369
401
|
const userRepository = await this.getUserRepository();
|
|
370
402
|
const user = await userRepository.findOne({
|
|
371
403
|
where: {
|
|
@@ -381,7 +413,6 @@ export class AuthenticationService {
|
|
|
381
413
|
if (!permissionRepository || !companyRepository || !branchRepository) {
|
|
382
414
|
throw new BadRequestException('Repositories not available');
|
|
383
415
|
}
|
|
384
|
-
// Validate company access
|
|
385
416
|
const hasCompanyAccess = await permissionRepository.findOne({
|
|
386
417
|
where: {
|
|
387
418
|
userId,
|
|
@@ -401,7 +432,6 @@ export class AuthenticationService {
|
|
|
401
432
|
if (!company) {
|
|
402
433
|
throw new UnauthorizedException('Company not found or inactive');
|
|
403
434
|
}
|
|
404
|
-
// Validate branch access
|
|
405
435
|
const hasBranchAccess = await permissionRepository.findOne({
|
|
406
436
|
where: {
|
|
407
437
|
userId,
|
|
@@ -424,8 +454,7 @@ export class AuthenticationService {
|
|
|
424
454
|
}
|
|
425
455
|
return this.generateLoginResponseWithCompany(user, company, branch);
|
|
426
456
|
}
|
|
427
|
-
|
|
428
|
-
/** Refresh access token (with token rotation). Returns new access token and new refresh token. */ async refreshToken(refreshToken) {
|
|
457
|
+
async refreshToken(refreshToken) {
|
|
429
458
|
try {
|
|
430
459
|
const payload = this.jwtService.verify(refreshToken, {
|
|
431
460
|
secret: this.authConfig.getRefreshTokenSecret()
|
|
@@ -459,7 +488,7 @@ export class AuthenticationService {
|
|
|
459
488
|
throw new UnauthorizedException('Invalid or expired refresh token');
|
|
460
489
|
}
|
|
461
490
|
}
|
|
462
|
-
|
|
491
|
+
async changePassword(userId, currentPassword, newPassword) {
|
|
463
492
|
const userRepository = await this.getUserRepository();
|
|
464
493
|
const user = await userRepository.findOne({
|
|
465
494
|
where: {
|
|
@@ -473,21 +502,19 @@ export class AuthenticationService {
|
|
|
473
502
|
user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
|
|
474
503
|
await userRepository.save(user);
|
|
475
504
|
}
|
|
476
|
-
|
|
505
|
+
async logout(userId) {
|
|
477
506
|
if (!this.cacheManager) {
|
|
478
|
-
// Cache is required for secure logout - tokens cannot be revoked without it
|
|
479
507
|
this.logger.error(`SECURITY: Cache not available for logout (userId: ${userId}). ` + 'Token revocation failed - tokens will remain valid until expiry. ' + 'Configure cache (Redis) to enable proper token revocation.');
|
|
480
508
|
throw new BadRequestException('Logout service temporarily unavailable. Please try again later.');
|
|
481
509
|
}
|
|
482
|
-
const revokedAt = Math.floor(Date.now() / 1000);
|
|
510
|
+
const revokedAt = Math.floor(Date.now() / 1000);
|
|
483
511
|
const cacheKey = `${TOKEN_REVOKED_PREFIX}:${userId}`;
|
|
484
|
-
// TTL matches refresh token expiry (tokens older than this are already expired)
|
|
485
512
|
const refreshTokenExpiry = this.authConfig.getRefreshTokenExpiration();
|
|
486
513
|
const ttlMs = this.parseExpiryToMs(refreshTokenExpiry);
|
|
487
514
|
await this.cacheManager.set(cacheKey, revokedAt, ttlMs);
|
|
488
515
|
this.logger.log(`User ${userId} logged out - all tokens revoked`);
|
|
489
516
|
}
|
|
490
|
-
|
|
517
|
+
async isTokenRevoked(userId, tokenIssuedAt) {
|
|
491
518
|
if (!this.cacheManager || !tokenIssuedAt) {
|
|
492
519
|
return false;
|
|
493
520
|
}
|
|
@@ -496,12 +523,11 @@ export class AuthenticationService {
|
|
|
496
523
|
if (!revokedAt) {
|
|
497
524
|
return false;
|
|
498
525
|
}
|
|
499
|
-
// Token is revoked if it was issued before the revocation timestamp
|
|
500
526
|
return tokenIssuedAt < revokedAt;
|
|
501
527
|
}
|
|
502
|
-
|
|
528
|
+
parseExpiryToMs(expiry) {
|
|
503
529
|
const match = expiry.match(/^(\d+)([smhd])$/);
|
|
504
|
-
if (!match) return 7 * 24 * 60 * 60 * 1000;
|
|
530
|
+
if (!match) return 7 * 24 * 60 * 60 * 1000;
|
|
505
531
|
const value = parseInt(match[1], 10);
|
|
506
532
|
const unit = match[2];
|
|
507
533
|
switch(unit){
|
|
@@ -517,8 +543,7 @@ export class AuthenticationService {
|
|
|
517
543
|
return 7 * 24 * 60 * 60 * 1000;
|
|
518
544
|
}
|
|
519
545
|
}
|
|
520
|
-
|
|
521
|
-
/** Get user's companies */ async getUserCompanies(userId) {
|
|
546
|
+
async getUserCompanies(userId) {
|
|
522
547
|
this.ensureCompanyFeatureEnabled();
|
|
523
548
|
const permissionRepository = await this.getPermissionRepository();
|
|
524
549
|
const companyRepository = await this.getCompanyRepository();
|
|
@@ -539,14 +564,13 @@ export class AuthenticationService {
|
|
|
539
564
|
active: true
|
|
540
565
|
}).orderBy('c.name', 'ASC').getMany();
|
|
541
566
|
}
|
|
542
|
-
|
|
567
|
+
async getUserBranches(userId, companyId) {
|
|
543
568
|
this.ensureCompanyFeatureEnabled();
|
|
544
569
|
const permissionRepository = await this.getPermissionRepository();
|
|
545
570
|
const branchRepository = await this.getBranchRepository();
|
|
546
571
|
if (!permissionRepository || !branchRepository) {
|
|
547
572
|
throw new BadRequestException('Repositories not available');
|
|
548
573
|
}
|
|
549
|
-
// Verify company access
|
|
550
574
|
const companyPermission = await permissionRepository.findOne({
|
|
551
575
|
where: {
|
|
552
576
|
userId,
|
|
@@ -557,7 +581,6 @@ export class AuthenticationService {
|
|
|
557
581
|
if (!companyPermission) {
|
|
558
582
|
throw new UnauthorizedException('No access to this company');
|
|
559
583
|
}
|
|
560
|
-
// Get branch permissions
|
|
561
584
|
const branchPermissions = await permissionRepository.find({
|
|
562
585
|
where: {
|
|
563
586
|
userId,
|
|
@@ -574,7 +597,7 @@ export class AuthenticationService {
|
|
|
574
597
|
active: true
|
|
575
598
|
}).orderBy('b.serial', 'ASC').addOrderBy('b.name', 'ASC').getMany();
|
|
576
599
|
}
|
|
577
|
-
|
|
600
|
+
async getCompanyById(companyId) {
|
|
578
601
|
const companyRepository = await this.getCompanyRepository();
|
|
579
602
|
if (!companyRepository) {
|
|
580
603
|
return null;
|
|
@@ -585,7 +608,7 @@ export class AuthenticationService {
|
|
|
585
608
|
}
|
|
586
609
|
});
|
|
587
610
|
}
|
|
588
|
-
|
|
611
|
+
async getBranchById(branchId) {
|
|
589
612
|
const branchRepository = await this.getBranchRepository();
|
|
590
613
|
if (!branchRepository) {
|
|
591
614
|
return null;
|
|
@@ -596,7 +619,7 @@ export class AuthenticationService {
|
|
|
596
619
|
}
|
|
597
620
|
});
|
|
598
621
|
}
|
|
599
|
-
|
|
622
|
+
async getUserInfo(userId, companyId, branchId) {
|
|
600
623
|
const userRepository = await this.getUserRepository();
|
|
601
624
|
const user = await userRepository.findOne({
|
|
602
625
|
where: {
|
|
@@ -622,7 +645,6 @@ export class AuthenticationService {
|
|
|
622
645
|
}
|
|
623
646
|
return response;
|
|
624
647
|
}
|
|
625
|
-
// Helper Methods
|
|
626
648
|
ensureCompanyFeatureEnabled() {
|
|
627
649
|
if (!this.isCompanyFeatureEnabled()) {
|
|
628
650
|
throw new BadRequestException('Company feature is not enabled');
|
|
@@ -642,8 +664,7 @@ export class AuthenticationService {
|
|
|
642
664
|
await userRepository.save(user);
|
|
643
665
|
return user;
|
|
644
666
|
}
|
|
645
|
-
|
|
646
|
-
/** Create user info object */ toUserInfo(user) {
|
|
667
|
+
toUserInfo(user) {
|
|
647
668
|
return {
|
|
648
669
|
id: user.id,
|
|
649
670
|
email: user.email,
|
|
@@ -652,7 +673,7 @@ export class AuthenticationService {
|
|
|
652
673
|
profilePictureId: user.profilePictureId ?? null
|
|
653
674
|
};
|
|
654
675
|
}
|
|
655
|
-
|
|
676
|
+
toCompanyInfo(company) {
|
|
656
677
|
return {
|
|
657
678
|
id: company.id,
|
|
658
679
|
name: company.name,
|
|
@@ -660,7 +681,7 @@ export class AuthenticationService {
|
|
|
660
681
|
logoId: company.logoId ?? null
|
|
661
682
|
};
|
|
662
683
|
}
|
|
663
|
-
|
|
684
|
+
toBranchInfo(branch) {
|
|
664
685
|
return {
|
|
665
686
|
id: branch.id,
|
|
666
687
|
name: branch.name,
|
|
@@ -668,7 +689,7 @@ export class AuthenticationService {
|
|
|
668
689
|
logoId: branch.logoId ?? null
|
|
669
690
|
};
|
|
670
691
|
}
|
|
671
|
-
|
|
692
|
+
generateTokens(payload) {
|
|
672
693
|
const accessToken = this.jwtService.sign({
|
|
673
694
|
...payload,
|
|
674
695
|
type: 'access'
|
|
@@ -694,6 +715,7 @@ export class AuthenticationService {
|
|
|
694
715
|
email: user.email,
|
|
695
716
|
type: 'access'
|
|
696
717
|
});
|
|
718
|
+
this.sendLoginNotification(user.id);
|
|
697
719
|
return {
|
|
698
720
|
accessToken,
|
|
699
721
|
refreshToken,
|
|
@@ -708,6 +730,7 @@ export class AuthenticationService {
|
|
|
708
730
|
companyId: company.id,
|
|
709
731
|
branchId: branch?.id
|
|
710
732
|
});
|
|
733
|
+
this.sendLoginNotification(user.id, company.id);
|
|
711
734
|
return {
|
|
712
735
|
accessToken,
|
|
713
736
|
refreshToken,
|
|
@@ -716,6 +739,18 @@ export class AuthenticationService {
|
|
|
716
739
|
branch: branch ? this.toBranchInfo(branch) : undefined
|
|
717
740
|
};
|
|
718
741
|
}
|
|
742
|
+
sendLoginNotification(userId, companyId) {
|
|
743
|
+
if (!this.notificationAdapter) return;
|
|
744
|
+
this.notificationAdapter.send({
|
|
745
|
+
userId,
|
|
746
|
+
title: 'Login successful',
|
|
747
|
+
message: 'You have successfully logged in to your account.',
|
|
748
|
+
type: NotificationType.INFO,
|
|
749
|
+
companyId
|
|
750
|
+
}).catch((error)=>{
|
|
751
|
+
this.logger.warn(`Failed to send login notification: ${error.message}`);
|
|
752
|
+
});
|
|
753
|
+
}
|
|
719
754
|
async generateCompanySelectionResponse(user, companies, branches) {
|
|
720
755
|
if (!this.sessionService) {
|
|
721
756
|
throw new BadRequestException('Session service not available');
|
|
@@ -750,13 +785,15 @@ export class AuthenticationService {
|
|
|
750
785
|
};
|
|
751
786
|
}
|
|
752
787
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
753
|
-
constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService){
|
|
788
|
+
constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService, userEnricher, notificationAdapter){
|
|
754
789
|
_define_property(this, "jwtService", void 0);
|
|
755
790
|
_define_property(this, "authConfig", void 0);
|
|
756
791
|
_define_property(this, "cacheManager", void 0);
|
|
757
792
|
_define_property(this, "dataSourceProvider", void 0);
|
|
758
793
|
_define_property(this, "sessionService", void 0);
|
|
759
794
|
_define_property(this, "authEmailService", void 0);
|
|
795
|
+
_define_property(this, "userEnricher", void 0);
|
|
796
|
+
_define_property(this, "notificationAdapter", void 0);
|
|
760
797
|
_define_property(this, "logger", void 0);
|
|
761
798
|
this.jwtService = jwtService;
|
|
762
799
|
this.authConfig = authConfig;
|
|
@@ -764,6 +801,8 @@ export class AuthenticationService {
|
|
|
764
801
|
this.dataSourceProvider = dataSourceProvider;
|
|
765
802
|
this.sessionService = sessionService;
|
|
766
803
|
this.authEmailService = authEmailService;
|
|
804
|
+
this.userEnricher = userEnricher;
|
|
805
|
+
this.notificationAdapter = notificationAdapter;
|
|
767
806
|
this.logger = new Logger(AuthenticationService.name);
|
|
768
807
|
}
|
|
769
808
|
}
|
|
@@ -781,6 +820,10 @@ AuthenticationService = _ts_decorate([
|
|
|
781
820
|
_ts_param(4, Inject(CompanySelectionSessionService)),
|
|
782
821
|
_ts_param(5, Optional()),
|
|
783
822
|
_ts_param(5, Inject(AuthEmailService)),
|
|
823
|
+
_ts_param(6, Optional()),
|
|
824
|
+
_ts_param(6, Inject(USER_ENRICHER)),
|
|
825
|
+
_ts_param(7, Optional()),
|
|
826
|
+
_ts_param(7, Inject(NOTIFICATION_ADAPTER)),
|
|
784
827
|
_ts_metadata("design:type", Function),
|
|
785
828
|
_ts_metadata("design:paramtypes", [
|
|
786
829
|
typeof JwtService === "undefined" ? Object : JwtService,
|
|
@@ -788,6 +831,8 @@ AuthenticationService = _ts_decorate([
|
|
|
788
831
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
789
832
|
typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
|
|
790
833
|
typeof CompanySelectionSessionService === "undefined" ? Object : CompanySelectionSessionService,
|
|
791
|
-
typeof AuthEmailService === "undefined" ? Object : AuthEmailService
|
|
834
|
+
typeof AuthEmailService === "undefined" ? Object : AuthEmailService,
|
|
835
|
+
typeof IUserEnricher === "undefined" ? Object : IUserEnricher,
|
|
836
|
+
typeof INotificationAdapter === "undefined" ? Object : INotificationAdapter
|
|
792
837
|
])
|
|
793
838
|
], AuthenticationService);
|