@flusys/nestjs-auth 0.1.0-beta.3 → 1.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 +274 -301
- package/cjs/config/auth.constants.js +22 -4
- package/cjs/controllers/authentication.controller.js +222 -38
- package/cjs/controllers/branch.controller.js +46 -4
- package/cjs/controllers/company-selection.controller.js +27 -20
- package/cjs/controllers/company.controller.js +46 -4
- package/cjs/controllers/user-permission.controller.js +40 -37
- package/cjs/controllers/user.controller.js +222 -23
- package/cjs/dtos/authentication.dto.js +119 -13
- package/cjs/dtos/user-permission.dto.js +28 -0
- package/cjs/dtos/user.dto.js +19 -4
- package/cjs/entities/index.js +5 -1
- package/cjs/entities/pending-registration.entity.js +125 -0
- package/cjs/entities/user-company-permission.entity.js +0 -6
- package/cjs/entities/user-token.entity.js +129 -0
- package/cjs/guards/auth-throttler.guard.js +53 -0
- package/cjs/interceptors/clear-token.interceptor.js +3 -6
- package/cjs/interceptors/set-token.interceptor.js +38 -31
- package/cjs/interfaces/auth-email-provider.interface.js +45 -0
- package/cjs/interfaces/authentication.interface.js +0 -3
- package/cjs/interfaces/index.js +2 -0
- package/cjs/interfaces/user-enricher.interface.js +4 -0
- package/cjs/modules/auth.module.js +8 -20
- package/cjs/services/auth-config.service.js +42 -47
- package/cjs/services/auth-datasource.provider.js +18 -34
- package/cjs/services/auth-email.service.js +471 -0
- package/cjs/services/authentication.service.js +240 -153
- package/cjs/services/branch.service.js +15 -48
- package/cjs/services/company-selection-session.service.js +1 -1
- package/cjs/services/company.service.js +7 -36
- package/cjs/services/index.js +1 -0
- package/cjs/services/user-permission.service.js +9 -29
- package/cjs/services/user.service.js +228 -107
- package/cjs/strategies/jwt.strategy.js +14 -1
- package/cjs/validators/password.validator.js +45 -0
- package/config/auth.constants.d.ts +5 -0
- package/controllers/authentication.controller.d.ts +13 -9
- package/controllers/company-selection.controller.d.ts +6 -2
- package/controllers/user.controller.d.ts +33 -0
- package/dtos/authentication.dto.d.ts +21 -3
- package/dtos/company-branch.dto.d.ts +0 -1
- package/dtos/company.dto.d.ts +0 -1
- package/dtos/user-permission.dto.d.ts +4 -0
- package/dtos/user.dto.d.ts +1 -2
- package/entities/index.d.ts +7 -3
- package/entities/pending-registration.entity.d.ts +17 -0
- package/entities/user-company-permission.entity.d.ts +0 -2
- package/entities/user-token.entity.d.ts +15 -0
- package/fesm/config/auth.constants.js +11 -8
- package/fesm/controllers/authentication.controller.js +224 -40
- package/fesm/controllers/branch.controller.js +49 -7
- package/fesm/controllers/company-selection.controller.js +28 -21
- package/fesm/controllers/company.controller.js +49 -7
- package/fesm/controllers/user-permission.controller.js +41 -38
- package/fesm/controllers/user.controller.js +225 -26
- package/fesm/dtos/authentication.dto.js +123 -111
- package/fesm/dtos/user-permission.dto.js +28 -0
- package/fesm/dtos/user.dto.js +20 -5
- package/fesm/entities/index.js +7 -1
- package/fesm/entities/pending-registration.entity.js +115 -0
- package/fesm/entities/user-company-permission.entity.js +0 -6
- package/fesm/entities/user-token.entity.js +113 -0
- package/fesm/guards/auth-throttler.guard.js +43 -0
- package/fesm/interceptors/clear-token.interceptor.js +2 -5
- package/fesm/interceptors/set-token.interceptor.js +37 -30
- package/fesm/interfaces/auth-email-provider.interface.js +38 -0
- package/fesm/interfaces/authentication.interface.js +1 -7
- package/fesm/interfaces/index.js +2 -0
- package/fesm/interfaces/user-enricher.interface.js +26 -0
- package/fesm/modules/auth.module.js +10 -22
- package/fesm/services/auth-config.service.js +42 -47
- package/fesm/services/auth-datasource.provider.js +19 -35
- package/fesm/services/auth-email.service.js +420 -0
- package/fesm/services/authentication.service.js +238 -151
- package/fesm/services/branch.service.js +16 -49
- package/fesm/services/company-selection-session.service.js +1 -1
- package/fesm/services/company.service.js +8 -37
- package/fesm/services/index.js +1 -0
- package/fesm/services/user-permission.service.js +10 -30
- package/fesm/services/user.service.js +229 -108
- package/fesm/strategies/jwt.strategy.js +14 -1
- package/fesm/validators/password.validator.js +48 -0
- package/guards/auth-throttler.guard.d.ts +7 -0
- package/interceptors/set-token.interceptor.d.ts +1 -0
- package/interfaces/auth-email-provider.interface.d.ts +6 -0
- package/interfaces/auth-module-options.interface.d.ts +4 -1
- package/interfaces/authentication.interface.d.ts +14 -0
- package/interfaces/index.d.ts +2 -0
- package/interfaces/user-enricher.interface.d.ts +64 -0
- package/interfaces/user.interface.d.ts +1 -0
- package/modules/auth.module.d.ts +0 -1
- package/package.json +4 -4
- package/services/auth-config.service.d.ts +5 -1
- package/services/auth-email.service.d.ts +51 -0
- package/services/authentication.service.d.ts +21 -12
- package/services/branch.service.d.ts +2 -5
- package/services/company-selection-session.service.d.ts +1 -1
- package/services/company.service.d.ts +1 -4
- package/services/index.d.ts +1 -0
- package/services/user-permission.service.d.ts +0 -1
- package/services/user.service.d.ts +20 -6
- package/validators/password.validator.d.ts +10 -0
|
@@ -27,74 +27,59 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Optional, Scope, UnauthorizedException } from '@nestjs/common';
|
|
29
29
|
import { JwtService } from '@nestjs/jwt';
|
|
30
|
-
import { InjectRepository } from '@nestjs/typeorm';
|
|
31
30
|
import * as bcrypt from 'bcrypt';
|
|
32
|
-
import { Repository } from 'typeorm';
|
|
33
31
|
import { ErrorHandler } from '@flusys/nestjs-shared/utils';
|
|
34
32
|
import { AuthConfigService } from './auth-config.service';
|
|
35
33
|
import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
36
34
|
import { PermissionType } from '../enums';
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX } from '../config';
|
|
36
|
+
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
37
|
+
import { AuthEmailService } from './auth-email.service';
|
|
39
38
|
import { CompanyBranch } from '../entities/company-branch.entity';
|
|
40
39
|
import { Company } from '../entities/company.entity';
|
|
41
40
|
import { UserCompanyPermission } from '../entities/user-company-permission.entity';
|
|
42
41
|
import { User } from '../entities/user.entity';
|
|
43
42
|
import { CompanySelectionSessionService } from './company-selection-session.service';
|
|
44
43
|
export class AuthenticationService {
|
|
45
|
-
//
|
|
46
|
-
// REPOSITORY GETTERS
|
|
47
|
-
// ==========================================================================
|
|
44
|
+
// Repository Getters
|
|
48
45
|
async getUserRepository() {
|
|
49
|
-
if (this.dataSourceProvider) {
|
|
50
|
-
|
|
46
|
+
if (!this.dataSourceProvider) {
|
|
47
|
+
throw new Error('User repository not available');
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
return this.injectedUserRepository;
|
|
54
|
-
}
|
|
55
|
-
throw new Error('User repository not available');
|
|
49
|
+
return this.dataSourceProvider.getRepository(User);
|
|
56
50
|
}
|
|
57
51
|
async getPermissionRepository() {
|
|
58
|
-
if (!this.authConfig.isCompanyFeatureEnabled()) return null;
|
|
59
|
-
|
|
60
|
-
return this.dataSourceProvider.getRepository(UserCompanyPermission);
|
|
61
|
-
}
|
|
62
|
-
return this.injectedPermissionRepository ?? null;
|
|
52
|
+
if (!this.authConfig.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
|
|
53
|
+
return this.dataSourceProvider.getRepository(UserCompanyPermission);
|
|
63
54
|
}
|
|
64
55
|
async getCompanyRepository() {
|
|
65
|
-
if (!this.authConfig.isCompanyFeatureEnabled()) return null;
|
|
66
|
-
|
|
67
|
-
return this.dataSourceProvider.getRepository(Company);
|
|
68
|
-
}
|
|
69
|
-
return this.injectedCompanyRepository ?? null;
|
|
56
|
+
if (!this.authConfig.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
|
|
57
|
+
return this.dataSourceProvider.getRepository(Company);
|
|
70
58
|
}
|
|
71
59
|
async getBranchRepository() {
|
|
72
|
-
if (!this.authConfig.isCompanyFeatureEnabled()) return null;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
// ==========================================================================
|
|
79
|
-
// LOGIN
|
|
80
|
-
// ==========================================================================
|
|
81
|
-
/**
|
|
82
|
-
* Login with email and password
|
|
83
|
-
* Returns either tokens directly or requires company/branch selection
|
|
84
|
-
*/ async login(email, password) {
|
|
60
|
+
if (!this.authConfig.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
|
|
61
|
+
return this.dataSourceProvider.getRepository(CompanyBranch);
|
|
62
|
+
}
|
|
63
|
+
// Login
|
|
64
|
+
/** Login with email and password. Returns tokens or requires company/branch selection. */ async login(email, password) {
|
|
85
65
|
const user = await this.validateUser(email, password);
|
|
86
66
|
if (!user.isActive) {
|
|
87
67
|
throw new UnauthorizedException('User account is deactivated');
|
|
88
68
|
}
|
|
89
|
-
//
|
|
69
|
+
// Check email verification if email service is enabled
|
|
70
|
+
if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
|
|
71
|
+
return {
|
|
72
|
+
requiresEmailVerification: true,
|
|
73
|
+
email: user.email,
|
|
74
|
+
message: 'Please verify your email address before logging in.'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
90
77
|
if (!this.authConfig.isCompanyFeatureEnabled()) {
|
|
91
78
|
return this.generateLoginResponse(user);
|
|
92
79
|
}
|
|
93
80
|
return this.handleCompanyBasedLogin(user);
|
|
94
81
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Handle login when company feature is enabled
|
|
97
|
-
*/ async handleCompanyBasedLogin(user) {
|
|
82
|
+
/** Handle login when company feature is enabled */ async handleCompanyBasedLogin(user) {
|
|
98
83
|
const permissionRepository = await this.getPermissionRepository();
|
|
99
84
|
const branchRepository = await this.getBranchRepository();
|
|
100
85
|
if (!permissionRepository || !branchRepository) {
|
|
@@ -144,16 +129,9 @@ export class AuthenticationService {
|
|
|
144
129
|
// Returns all companies with their branches for frontend to display
|
|
145
130
|
return await this.generateCompanySelectionResponse(user, companies, branches);
|
|
146
131
|
}
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
// ==========================================================================
|
|
150
|
-
/**
|
|
151
|
-
* Register a new user
|
|
152
|
-
* If company feature enabled, optionally join/create company
|
|
153
|
-
*/ async register(dto) {
|
|
132
|
+
// Registration
|
|
133
|
+
/** Register a new user. If email enabled, creates pending registration. Otherwise creates user directly. */ async register(dto, verifyUrlBase) {
|
|
154
134
|
const userRepository = await this.getUserRepository();
|
|
155
|
-
// Check if user exists
|
|
156
|
-
// Note: Generic error message to prevent email enumeration attacks
|
|
157
135
|
const existingUser = await userRepository.findOne({
|
|
158
136
|
where: {
|
|
159
137
|
email: dto.email
|
|
@@ -162,7 +140,33 @@ export class AuthenticationService {
|
|
|
162
140
|
if (existingUser) {
|
|
163
141
|
throw new BadRequestException('Registration failed. Please try again or contact support.');
|
|
164
142
|
}
|
|
165
|
-
//
|
|
143
|
+
// If email verification is enabled, create pending registration instead of user
|
|
144
|
+
if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
|
|
145
|
+
const companyData = this.authConfig.isCompanyFeatureEnabled() ? {
|
|
146
|
+
companySlug: dto.companySlug,
|
|
147
|
+
branchSlug: dto.branchSlug,
|
|
148
|
+
newCompanyName: dto.newCompanyName,
|
|
149
|
+
newCompanyPhone: dto.newCompanyPhone,
|
|
150
|
+
newCompanyAddress: dto.newCompanyAddress
|
|
151
|
+
} : undefined;
|
|
152
|
+
const result = await this.authEmailService.createPendingRegistration({
|
|
153
|
+
email: dto.email,
|
|
154
|
+
name: dto.name,
|
|
155
|
+
phone: dto.phone,
|
|
156
|
+
password: dto.password,
|
|
157
|
+
companyData,
|
|
158
|
+
verifyUrlBase
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
requiresEmailVerification: true,
|
|
162
|
+
message: result.message
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// No email verification - create user directly
|
|
166
|
+
return this.createUserDirectly(dto);
|
|
167
|
+
}
|
|
168
|
+
/** Create user directly (no email verification) */ async createUserDirectly(dto) {
|
|
169
|
+
const userRepository = await this.getUserRepository();
|
|
166
170
|
const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
167
171
|
const user = userRepository.create({
|
|
168
172
|
name: dto.name,
|
|
@@ -174,40 +178,68 @@ export class AuthenticationService {
|
|
|
174
178
|
phoneVerified: false
|
|
175
179
|
});
|
|
176
180
|
await userRepository.save(user);
|
|
177
|
-
// If company feature not enabled, return simple response
|
|
178
181
|
if (!this.authConfig.isCompanyFeatureEnabled()) {
|
|
179
|
-
return this.
|
|
182
|
+
return this.generateRegistrationResponse(user, false, false);
|
|
180
183
|
}
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
return this.handleCompanyRegistration(user, dto, false);
|
|
185
|
+
}
|
|
186
|
+
/** Create user from pending registration (after email verification) */ async createUserFromPendingRegistration(pending) {
|
|
187
|
+
const userRepository = await this.getUserRepository();
|
|
188
|
+
// Check if user already exists (race condition protection)
|
|
189
|
+
const existingUser = await userRepository.findOne({
|
|
190
|
+
where: {
|
|
191
|
+
email: pending.email
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
if (existingUser) {
|
|
195
|
+
throw new BadRequestException('This email has already been registered.');
|
|
196
|
+
}
|
|
197
|
+
// Create user with verified email
|
|
198
|
+
const user = userRepository.create({
|
|
199
|
+
name: pending.name,
|
|
200
|
+
email: pending.email,
|
|
201
|
+
phone: pending.phone,
|
|
202
|
+
password: pending.passwordHash,
|
|
203
|
+
isActive: true,
|
|
204
|
+
emailVerified: true,
|
|
205
|
+
phoneVerified: false
|
|
206
|
+
});
|
|
207
|
+
await userRepository.save(user);
|
|
208
|
+
// Handle company feature if enabled
|
|
209
|
+
if (this.authConfig.isCompanyFeatureEnabled() && pending.companyData) {
|
|
210
|
+
return this.handleCompanyRegistrationFromPending(user, pending.companyData);
|
|
211
|
+
}
|
|
212
|
+
return this.generateRegistrationResponse(user, this.authConfig.isCompanyFeatureEnabled(), false);
|
|
213
|
+
}
|
|
214
|
+
/** Handle company registration from pending registration data */ async handleCompanyRegistrationFromPending(user, companyData) {
|
|
215
|
+
return this.processCompanyRegistration(user, companyData, false);
|
|
183
216
|
}
|
|
184
|
-
/**
|
|
185
|
-
|
|
186
|
-
|
|
217
|
+
/** Handle company-related registration logic */ async handleCompanyRegistration(user, dto, requiresEmailVerification) {
|
|
218
|
+
return this.processCompanyRegistration(user, dto, requiresEmailVerification);
|
|
219
|
+
}
|
|
220
|
+
/** Shared logic for company registration (join existing or create new) */ async processCompanyRegistration(user, data, requiresEmailVerification) {
|
|
187
221
|
const companyRepository = await this.getCompanyRepository();
|
|
188
222
|
const branchRepository = await this.getBranchRepository();
|
|
189
223
|
const permissionRepository = await this.getPermissionRepository();
|
|
190
224
|
if (!companyRepository || !branchRepository || !permissionRepository) {
|
|
191
|
-
return this.
|
|
225
|
+
return this.generateRegistrationResponse(user, true, requiresEmailVerification);
|
|
192
226
|
}
|
|
193
227
|
let company = null;
|
|
194
228
|
let branch = null;
|
|
195
|
-
|
|
196
|
-
if (dto.companySlug) {
|
|
229
|
+
if (data.companySlug) {
|
|
197
230
|
company = await companyRepository.findOne({
|
|
198
231
|
where: {
|
|
199
|
-
slug:
|
|
232
|
+
slug: data.companySlug,
|
|
200
233
|
isActive: true
|
|
201
234
|
}
|
|
202
235
|
});
|
|
203
236
|
if (!company) {
|
|
204
237
|
throw new BadRequestException('Company not found or inactive');
|
|
205
238
|
}
|
|
206
|
-
|
|
207
|
-
if (dto.branchSlug) {
|
|
239
|
+
if (data.branchSlug) {
|
|
208
240
|
branch = await branchRepository.findOne({
|
|
209
241
|
where: {
|
|
210
|
-
slug:
|
|
242
|
+
slug: data.branchSlug,
|
|
211
243
|
companyId: company.id,
|
|
212
244
|
isActive: true
|
|
213
245
|
}
|
|
@@ -216,7 +248,6 @@ export class AuthenticationService {
|
|
|
216
248
|
throw new BadRequestException('Branch not found or inactive');
|
|
217
249
|
}
|
|
218
250
|
} else {
|
|
219
|
-
// Get first active branch
|
|
220
251
|
branch = await branchRepository.findOne({
|
|
221
252
|
where: {
|
|
222
253
|
companyId: company.id,
|
|
@@ -228,9 +259,8 @@ export class AuthenticationService {
|
|
|
228
259
|
}
|
|
229
260
|
});
|
|
230
261
|
}
|
|
231
|
-
} else if (
|
|
232
|
-
const slug = this.generateSlug(
|
|
233
|
-
// Check if slug exists
|
|
262
|
+
} else if (data.newCompanyName) {
|
|
263
|
+
const slug = this.generateSlug(data.newCompanyName);
|
|
234
264
|
const existingCompany = await companyRepository.findOne({
|
|
235
265
|
where: {
|
|
236
266
|
slug
|
|
@@ -239,16 +269,14 @@ export class AuthenticationService {
|
|
|
239
269
|
if (existingCompany) {
|
|
240
270
|
throw new BadRequestException('Company creation failed. Please try a different name.');
|
|
241
271
|
}
|
|
242
|
-
// Create company
|
|
243
272
|
company = companyRepository.create({
|
|
244
|
-
name:
|
|
273
|
+
name: data.newCompanyName,
|
|
245
274
|
slug,
|
|
246
|
-
phone:
|
|
247
|
-
address:
|
|
275
|
+
phone: data.newCompanyPhone,
|
|
276
|
+
address: data.newCompanyAddress,
|
|
248
277
|
isActive: true
|
|
249
278
|
});
|
|
250
279
|
await companyRepository.save(company);
|
|
251
|
-
// Create default branch
|
|
252
280
|
branch = branchRepository.create({
|
|
253
281
|
name: 'Main Branch',
|
|
254
282
|
slug: 'main-branch',
|
|
@@ -257,16 +285,13 @@ export class AuthenticationService {
|
|
|
257
285
|
});
|
|
258
286
|
await branchRepository.save(branch);
|
|
259
287
|
}
|
|
260
|
-
// Create permissions if company exists
|
|
261
288
|
if (company) {
|
|
262
|
-
// Create company permission
|
|
263
289
|
const companyPermission = permissionRepository.create({
|
|
264
290
|
userId: user.id,
|
|
265
291
|
permissionType: PermissionType.COMPANY,
|
|
266
292
|
targetId: company.id
|
|
267
293
|
});
|
|
268
294
|
await permissionRepository.save(companyPermission);
|
|
269
|
-
// Create branch permission if branch exists
|
|
270
295
|
if (branch) {
|
|
271
296
|
const branchPermission = permissionRepository.create({
|
|
272
297
|
userId: user.id,
|
|
@@ -275,23 +300,15 @@ export class AuthenticationService {
|
|
|
275
300
|
});
|
|
276
301
|
await permissionRepository.save(branchPermission);
|
|
277
302
|
}
|
|
278
|
-
return this.
|
|
303
|
+
return this.generateRegistrationResponseWithCompany(user, company, branch ?? undefined, requiresEmailVerification);
|
|
279
304
|
}
|
|
280
|
-
|
|
281
|
-
return this.generateLoginResponse(user, true);
|
|
305
|
+
return this.generateRegistrationResponse(user, true, requiresEmailVerification);
|
|
282
306
|
}
|
|
283
|
-
/**
|
|
284
|
-
* Generate URL-friendly slug from name
|
|
285
|
-
*/ generateSlug(name) {
|
|
307
|
+
/** Generate URL-friendly slug from name */ generateSlug(name) {
|
|
286
308
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 100);
|
|
287
309
|
}
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
// ==========================================================================
|
|
291
|
-
/**
|
|
292
|
-
* Select company and branch (step 2 of login)
|
|
293
|
-
* Frontend receives all options and user selects both together
|
|
294
|
-
*/ async select(sessionId, companyId, branchId) {
|
|
310
|
+
// Company/Branch Selection
|
|
311
|
+
/** Select company and branch (step 2 of login). Frontend receives all options and user selects both. */ async select(sessionId, companyId, branchId) {
|
|
295
312
|
this.ensureCompanyFeatureEnabled();
|
|
296
313
|
if (!this.sessionService) {
|
|
297
314
|
throw new BadRequestException('Session service not available');
|
|
@@ -358,9 +375,7 @@ export class AuthenticationService {
|
|
|
358
375
|
}
|
|
359
376
|
return this.generateLoginResponseWithCompany(user, company, branch);
|
|
360
377
|
}
|
|
361
|
-
/**
|
|
362
|
-
* Switch company/branch after login
|
|
363
|
-
*/ async switchCompany(userId, companyId, branchId) {
|
|
378
|
+
/** Switch company/branch after login */ async switchCompany(userId, companyId, branchId) {
|
|
364
379
|
this.ensureCompanyFeatureEnabled();
|
|
365
380
|
const userRepository = await this.getUserRepository();
|
|
366
381
|
const user = await userRepository.findOne({
|
|
@@ -419,17 +434,17 @@ export class AuthenticationService {
|
|
|
419
434
|
}
|
|
420
435
|
return this.generateLoginResponseWithCompany(user, company, branch);
|
|
421
436
|
}
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
// ==========================================================================
|
|
425
|
-
/**
|
|
426
|
-
* Refresh access token (with token rotation)
|
|
427
|
-
* Returns new access token and new refresh token
|
|
428
|
-
*/ async refreshToken(refreshToken) {
|
|
437
|
+
// Token Management
|
|
438
|
+
/** Refresh access token (with token rotation). Returns new access token and new refresh token. */ async refreshToken(refreshToken) {
|
|
429
439
|
try {
|
|
430
440
|
const payload = this.jwtService.verify(refreshToken, {
|
|
431
441
|
secret: this.authConfig.getRefreshTokenSecret()
|
|
432
442
|
});
|
|
443
|
+
// Check if token has been revoked (user logged out after token was issued)
|
|
444
|
+
const isRevoked = await this.isTokenRevoked(payload.id, payload.iat);
|
|
445
|
+
if (isRevoked) {
|
|
446
|
+
throw new UnauthorizedException('Token has been revoked');
|
|
447
|
+
}
|
|
433
448
|
const userRepository = await this.getUserRepository();
|
|
434
449
|
const user = await userRepository.findOne({
|
|
435
450
|
where: {
|
|
@@ -491,9 +506,7 @@ export class AuthenticationService {
|
|
|
491
506
|
throw new UnauthorizedException('Invalid or expired refresh token');
|
|
492
507
|
}
|
|
493
508
|
}
|
|
494
|
-
/**
|
|
495
|
-
* Change password
|
|
496
|
-
*/ async changePassword(userId, currentPassword, newPassword) {
|
|
509
|
+
/** Change password */ async changePassword(userId, currentPassword, newPassword) {
|
|
497
510
|
const userRepository = await this.getUserRepository();
|
|
498
511
|
const user = await userRepository.findOne({
|
|
499
512
|
where: {
|
|
@@ -510,15 +523,52 @@ export class AuthenticationService {
|
|
|
510
523
|
user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
|
|
511
524
|
await userRepository.save(user);
|
|
512
525
|
}
|
|
513
|
-
/**
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
526
|
+
/** Logout - Revokes all tokens. Stores revocation timestamp in cache. */ async logout(userId) {
|
|
527
|
+
if (!this.cacheManager) {
|
|
528
|
+
// Cache is required for secure logout - tokens cannot be revoked without it
|
|
529
|
+
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.');
|
|
530
|
+
throw new BadRequestException('Logout service temporarily unavailable. Please try again later.');
|
|
531
|
+
}
|
|
532
|
+
const revokedAt = Math.floor(Date.now() / 1000); // Unix timestamp in seconds
|
|
533
|
+
const cacheKey = `${TOKEN_REVOKED_PREFIX}:${userId}`;
|
|
534
|
+
// TTL matches refresh token expiry (tokens older than this are already expired)
|
|
535
|
+
const refreshTokenExpiry = this.authConfig.getRefreshTokenExpiration();
|
|
536
|
+
const ttlMs = this.parseExpiryToMs(refreshTokenExpiry);
|
|
537
|
+
await this.cacheManager.set(cacheKey, revokedAt, ttlMs);
|
|
538
|
+
this.logger.log(`User ${userId} logged out - all tokens revoked`);
|
|
539
|
+
}
|
|
540
|
+
/** Check if a token is revoked (issued before user's revocation timestamp) */ async isTokenRevoked(userId, tokenIssuedAt) {
|
|
541
|
+
if (!this.cacheManager || !tokenIssuedAt) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
const cacheKey = `${TOKEN_REVOKED_PREFIX}:${userId}`;
|
|
545
|
+
const revokedAt = await this.cacheManager.get(cacheKey);
|
|
546
|
+
if (!revokedAt) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
// Token is revoked if it was issued before the revocation timestamp
|
|
550
|
+
return tokenIssuedAt < revokedAt;
|
|
551
|
+
}
|
|
552
|
+
/** Parse JWT expiry string to milliseconds */ parseExpiryToMs(expiry) {
|
|
553
|
+
const match = expiry.match(/^(\d+)([smhd])$/);
|
|
554
|
+
if (!match) return 7 * 24 * 60 * 60 * 1000; // Default 7 days
|
|
555
|
+
const value = parseInt(match[1], 10);
|
|
556
|
+
const unit = match[2];
|
|
557
|
+
switch(unit){
|
|
558
|
+
case 's':
|
|
559
|
+
return value * 1000;
|
|
560
|
+
case 'm':
|
|
561
|
+
return value * 60 * 1000;
|
|
562
|
+
case 'h':
|
|
563
|
+
return value * 60 * 60 * 1000;
|
|
564
|
+
case 'd':
|
|
565
|
+
return value * 24 * 60 * 60 * 1000;
|
|
566
|
+
default:
|
|
567
|
+
return 7 * 24 * 60 * 60 * 1000;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Query Methods
|
|
571
|
+
/** Get user's companies */ async getUserCompanies(userId) {
|
|
522
572
|
this.ensureCompanyFeatureEnabled();
|
|
523
573
|
const permissionRepository = await this.getPermissionRepository();
|
|
524
574
|
const companyRepository = await this.getCompanyRepository();
|
|
@@ -539,9 +589,7 @@ export class AuthenticationService {
|
|
|
539
589
|
active: true
|
|
540
590
|
}).orderBy('c.name', 'ASC').getMany();
|
|
541
591
|
}
|
|
542
|
-
/**
|
|
543
|
-
* Get branches for a company that user has access to
|
|
544
|
-
*/ async getUserBranches(userId, companyId) {
|
|
592
|
+
/** Get branches for a company that user has access to */ async getUserBranches(userId, companyId) {
|
|
545
593
|
this.ensureCompanyFeatureEnabled();
|
|
546
594
|
const permissionRepository = await this.getPermissionRepository();
|
|
547
595
|
const branchRepository = await this.getBranchRepository();
|
|
@@ -576,9 +624,7 @@ export class AuthenticationService {
|
|
|
576
624
|
active: true
|
|
577
625
|
}).orderBy('b.serial', 'ASC').addOrderBy('b.name', 'ASC').getMany();
|
|
578
626
|
}
|
|
579
|
-
/**
|
|
580
|
-
* Get company by ID
|
|
581
|
-
*/ async getCompanyById(companyId) {
|
|
627
|
+
/** Get company by ID */ async getCompanyById(companyId) {
|
|
582
628
|
const companyRepository = await this.getCompanyRepository();
|
|
583
629
|
if (!companyRepository) {
|
|
584
630
|
return null;
|
|
@@ -589,9 +635,7 @@ export class AuthenticationService {
|
|
|
589
635
|
}
|
|
590
636
|
});
|
|
591
637
|
}
|
|
592
|
-
/**
|
|
593
|
-
* Get branch by ID
|
|
594
|
-
*/ async getBranchById(branchId) {
|
|
638
|
+
/** Get branch by ID */ async getBranchById(branchId) {
|
|
595
639
|
const branchRepository = await this.getBranchRepository();
|
|
596
640
|
if (!branchRepository) {
|
|
597
641
|
return null;
|
|
@@ -602,10 +646,7 @@ export class AuthenticationService {
|
|
|
602
646
|
}
|
|
603
647
|
});
|
|
604
648
|
}
|
|
605
|
-
/**
|
|
606
|
-
* Get user info for /auth/me endpoint
|
|
607
|
-
* Returns only user, company, and branch information (no tokens)
|
|
608
|
-
*/ async getUserInfo(userId, companyId, branchId) {
|
|
649
|
+
/** Get user info for /auth/me endpoint. Returns user, company, and branch info (no tokens). */ async getUserInfo(userId, companyId, branchId) {
|
|
609
650
|
const userRepository = await this.getUserRepository();
|
|
610
651
|
const user = await userRepository.findOne({
|
|
611
652
|
where: {
|
|
@@ -621,6 +662,7 @@ export class AuthenticationService {
|
|
|
621
662
|
id: user.id,
|
|
622
663
|
email: user.email,
|
|
623
664
|
name: user.name ?? undefined,
|
|
665
|
+
phone: user.phone ?? undefined,
|
|
624
666
|
profilePictureId: user.profilePictureId ?? undefined
|
|
625
667
|
}
|
|
626
668
|
};
|
|
@@ -656,9 +698,7 @@ export class AuthenticationService {
|
|
|
656
698
|
}
|
|
657
699
|
return response;
|
|
658
700
|
}
|
|
659
|
-
//
|
|
660
|
-
// HELPER METHODS
|
|
661
|
-
// ==========================================================================
|
|
701
|
+
// Helper Methods
|
|
662
702
|
ensureCompanyFeatureEnabled() {
|
|
663
703
|
if (!this.authConfig.isCompanyFeatureEnabled()) {
|
|
664
704
|
throw new BadRequestException('Company feature is not enabled');
|
|
@@ -682,9 +722,7 @@ export class AuthenticationService {
|
|
|
682
722
|
await userRepository.save(user);
|
|
683
723
|
return user;
|
|
684
724
|
}
|
|
685
|
-
//
|
|
686
|
-
// RESPONSE GENERATORS
|
|
687
|
-
// ==========================================================================
|
|
725
|
+
// Response Generators
|
|
688
726
|
generateLoginResponse(user, companyFeatureEnabled = false) {
|
|
689
727
|
const payload = {
|
|
690
728
|
id: user.id,
|
|
@@ -711,6 +749,7 @@ export class AuthenticationService {
|
|
|
711
749
|
id: user.id,
|
|
712
750
|
email: user.email,
|
|
713
751
|
name: user.name ?? undefined,
|
|
752
|
+
phone: user.phone ?? null,
|
|
714
753
|
profilePictureId: user.profilePictureId ?? null
|
|
715
754
|
},
|
|
716
755
|
companyFeatureEnabled
|
|
@@ -746,6 +785,7 @@ export class AuthenticationService {
|
|
|
746
785
|
id: user.id,
|
|
747
786
|
email: user.email,
|
|
748
787
|
name: user.name ?? undefined,
|
|
788
|
+
phone: user.phone ?? null,
|
|
749
789
|
profilePictureId: user.profilePictureId ?? null
|
|
750
790
|
},
|
|
751
791
|
company: {
|
|
@@ -791,30 +831,83 @@ export class AuthenticationService {
|
|
|
791
831
|
id: user.id,
|
|
792
832
|
email: user.email,
|
|
793
833
|
name: user.name ?? undefined,
|
|
834
|
+
phone: user.phone ?? null,
|
|
794
835
|
profilePictureId: user.profilePictureId ?? null
|
|
795
836
|
},
|
|
796
837
|
companies: companiesWithBranches
|
|
797
838
|
};
|
|
798
839
|
}
|
|
840
|
+
/** Generate registration response - tokens included only if email verification not required */ generateRegistrationResponse(user, companyFeatureEnabled, requiresEmailVerification) {
|
|
841
|
+
const userInfo = {
|
|
842
|
+
id: user.id,
|
|
843
|
+
email: user.email,
|
|
844
|
+
name: user.name ?? undefined,
|
|
845
|
+
phone: user.phone ?? null,
|
|
846
|
+
profilePictureId: user.profilePictureId ?? null
|
|
847
|
+
};
|
|
848
|
+
if (requiresEmailVerification) {
|
|
849
|
+
return {
|
|
850
|
+
user: userInfo,
|
|
851
|
+
companyFeatureEnabled,
|
|
852
|
+
requiresEmailVerification: true
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
const loginResponse = this.generateLoginResponse(user, companyFeatureEnabled);
|
|
856
|
+
return {
|
|
857
|
+
...loginResponse,
|
|
858
|
+
requiresEmailVerification: false
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/** Generate registration response with company - tokens included only if email verification not required */ generateRegistrationResponseWithCompany(user, company, branch, requiresEmailVerification = false) {
|
|
862
|
+
const userInfo = {
|
|
863
|
+
id: user.id,
|
|
864
|
+
email: user.email,
|
|
865
|
+
name: user.name ?? undefined,
|
|
866
|
+
phone: user.phone ?? null,
|
|
867
|
+
profilePictureId: user.profilePictureId ?? null
|
|
868
|
+
};
|
|
869
|
+
const companyInfo = {
|
|
870
|
+
id: company.id,
|
|
871
|
+
name: company.name,
|
|
872
|
+
slug: company.slug,
|
|
873
|
+
logoId: company.logoId ?? null
|
|
874
|
+
};
|
|
875
|
+
const branchInfo = branch ? {
|
|
876
|
+
id: branch.id,
|
|
877
|
+
name: branch.name,
|
|
878
|
+
slug: branch.slug,
|
|
879
|
+
logoId: branch.logoId ?? null
|
|
880
|
+
} : undefined;
|
|
881
|
+
if (requiresEmailVerification) {
|
|
882
|
+
return {
|
|
883
|
+
user: userInfo,
|
|
884
|
+
company: companyInfo,
|
|
885
|
+
branch: branchInfo,
|
|
886
|
+
companyFeatureEnabled: true,
|
|
887
|
+
requiresEmailVerification: true
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const loginResponse = this.generateLoginResponseWithCompany(user, company, branch);
|
|
891
|
+
return {
|
|
892
|
+
...loginResponse,
|
|
893
|
+
requiresEmailVerification: false
|
|
894
|
+
};
|
|
895
|
+
}
|
|
799
896
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
800
|
-
constructor(jwtService, authConfig, dataSourceProvider, sessionService,
|
|
897
|
+
constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService){
|
|
801
898
|
_define_property(this, "jwtService", void 0);
|
|
802
899
|
_define_property(this, "authConfig", void 0);
|
|
900
|
+
_define_property(this, "cacheManager", void 0);
|
|
803
901
|
_define_property(this, "dataSourceProvider", void 0);
|
|
804
902
|
_define_property(this, "sessionService", void 0);
|
|
805
|
-
_define_property(this, "
|
|
806
|
-
_define_property(this, "injectedPermissionRepository", void 0);
|
|
807
|
-
_define_property(this, "injectedCompanyRepository", void 0);
|
|
808
|
-
_define_property(this, "injectedBranchRepository", void 0);
|
|
903
|
+
_define_property(this, "authEmailService", void 0);
|
|
809
904
|
_define_property(this, "logger", void 0);
|
|
810
905
|
this.jwtService = jwtService;
|
|
811
906
|
this.authConfig = authConfig;
|
|
907
|
+
this.cacheManager = cacheManager;
|
|
812
908
|
this.dataSourceProvider = dataSourceProvider;
|
|
813
909
|
this.sessionService = sessionService;
|
|
814
|
-
this.
|
|
815
|
-
this.injectedPermissionRepository = injectedPermissionRepository;
|
|
816
|
-
this.injectedCompanyRepository = injectedCompanyRepository;
|
|
817
|
-
this.injectedBranchRepository = injectedBranchRepository;
|
|
910
|
+
this.authEmailService = authEmailService;
|
|
818
911
|
this.logger = new Logger(AuthenticationService.name);
|
|
819
912
|
}
|
|
820
913
|
}
|
|
@@ -825,26 +918,20 @@ AuthenticationService = _ts_decorate([
|
|
|
825
918
|
_ts_param(0, Inject(JwtService)),
|
|
826
919
|
_ts_param(1, Inject(AuthConfigService)),
|
|
827
920
|
_ts_param(2, Optional()),
|
|
828
|
-
_ts_param(2, Inject(
|
|
921
|
+
_ts_param(2, Inject('CACHE_INSTANCE')),
|
|
829
922
|
_ts_param(3, Optional()),
|
|
830
|
-
_ts_param(3, Inject(
|
|
923
|
+
_ts_param(3, Inject(AuthDataSourceProvider)),
|
|
831
924
|
_ts_param(4, Optional()),
|
|
832
|
-
_ts_param(4,
|
|
925
|
+
_ts_param(4, Inject(CompanySelectionSessionService)),
|
|
833
926
|
_ts_param(5, Optional()),
|
|
834
|
-
_ts_param(5,
|
|
835
|
-
_ts_param(6, Optional()),
|
|
836
|
-
_ts_param(6, InjectRepository(Company)),
|
|
837
|
-
_ts_param(7, Optional()),
|
|
838
|
-
_ts_param(7, InjectRepository(CompanyBranch)),
|
|
927
|
+
_ts_param(5, Inject(AuthEmailService)),
|
|
839
928
|
_ts_metadata("design:type", Function),
|
|
840
929
|
_ts_metadata("design:paramtypes", [
|
|
841
930
|
typeof JwtService === "undefined" ? Object : JwtService,
|
|
842
931
|
typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
|
|
932
|
+
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
843
933
|
typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
|
|
844
934
|
typeof CompanySelectionSessionService === "undefined" ? Object : CompanySelectionSessionService,
|
|
845
|
-
typeof
|
|
846
|
-
typeof Repository === "undefined" ? Object : Repository,
|
|
847
|
-
typeof Repository === "undefined" ? Object : Repository,
|
|
848
|
-
typeof Repository === "undefined" ? Object : Repository
|
|
935
|
+
typeof AuthEmailService === "undefined" ? Object : AuthEmailService
|
|
849
936
|
])
|
|
850
937
|
], AuthenticationService);
|