@flusys/nestjs-auth 1.0.0-rc → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +530 -396
- package/cjs/controllers/authentication.controller.js +160 -28
- package/cjs/controllers/user-permission.controller.js +49 -136
- package/cjs/docs/auth-swagger.config.js +14 -7
- package/cjs/dtos/authentication.dto.js +0 -30
- package/cjs/dtos/index.js +0 -1
- package/cjs/entities/user-company-permission.entity.js +3 -3
- package/cjs/interceptors/clear-token.interceptor.js +8 -18
- package/cjs/interceptors/set-token.interceptor.js +8 -55
- package/cjs/interfaces/auth-email-provider.interface.js +1 -1
- package/cjs/modules/auth.module.js +63 -129
- package/cjs/services/auth-config.service.js +6 -29
- package/cjs/services/auth-datasource.provider.js +8 -20
- package/cjs/services/auth-email.service.js +72 -87
- package/cjs/services/authentication.service.js +211 -355
- package/cjs/services/branch.service.js +9 -5
- package/cjs/services/user-permission.service.js +43 -71
- package/cjs/services/user.service.js +18 -47
- package/cjs/strategies/jwt.strategy.js +44 -44
- package/controllers/authentication.controller.d.ts +5 -3
- package/controllers/user-permission.controller.d.ts +5 -4
- package/dtos/authentication.dto.d.ts +0 -4
- package/dtos/index.d.ts +0 -1
- package/entities/index.d.ts +3 -3
- package/entities/user-company-permission.entity.d.ts +1 -1
- package/fesm/controllers/authentication.controller.js +162 -30
- package/fesm/controllers/user-permission.controller.js +50 -137
- package/fesm/docs/auth-swagger.config.js +14 -7
- package/fesm/dtos/authentication.dto.js +0 -30
- package/fesm/dtos/index.js +0 -1
- package/fesm/entities/user-company-permission.entity.js +3 -3
- package/fesm/interceptors/clear-token.interceptor.js +8 -18
- package/fesm/interceptors/set-token.interceptor.js +8 -55
- package/fesm/interfaces/auth-email-provider.interface.js +1 -1
- package/fesm/modules/auth.module.js +63 -129
- package/fesm/services/auth-config.service.js +6 -29
- package/fesm/services/auth-datasource.provider.js +8 -20
- package/fesm/services/auth-email.service.js +72 -87
- package/fesm/services/authentication.service.js +211 -355
- package/fesm/services/branch.service.js +9 -5
- package/fesm/services/user-permission.service.js +44 -72
- package/fesm/services/user.service.js +18 -47
- package/fesm/strategies/jwt.strategy.js +44 -44
- package/interceptors/clear-token.interceptor.d.ts +0 -1
- package/interceptors/set-token.interceptor.d.ts +1 -2
- package/interfaces/authentication.interface.d.ts +0 -4
- package/modules/auth.module.d.ts +6 -2
- package/package.json +3 -3
- package/services/auth-config.service.d.ts +2 -7
- package/services/auth-datasource.provider.d.ts +2 -4
- package/services/auth-email.service.d.ts +7 -4
- package/services/authentication.service.d.ts +14 -8
- package/services/user-permission.service.d.ts +6 -8
- package/services/user.service.d.ts +1 -1
- package/strategies/jwt.strategy.d.ts +4 -1
- package/cjs/dtos/user-company-permission.dto.js +0 -110
- package/cjs/guards/auth-throttler.guard.js +0 -53
- package/dtos/user-company-permission.dto.d.ts +0 -10
- package/fesm/dtos/user-company-permission.dto.js +0 -98
- package/fesm/guards/auth-throttler.guard.js +0 -43
- package/guards/auth-throttler.guard.d.ts +0 -7
|
@@ -25,22 +25,26 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
|
+
import { ErrorHandler, generateUniqueSlug } from '@flusys/nestjs-shared/utils';
|
|
28
30
|
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Optional, Scope, UnauthorizedException } from '@nestjs/common';
|
|
29
31
|
import { JwtService } from '@nestjs/jwt';
|
|
30
32
|
import * as bcrypt from 'bcrypt';
|
|
31
|
-
import { ErrorHandler } from '@flusys/nestjs-shared/utils';
|
|
32
|
-
import { AuthConfigService } from './auth-config.service';
|
|
33
|
-
import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
34
|
-
import { PermissionType } from '../enums';
|
|
35
33
|
import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX } from '../config';
|
|
36
|
-
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
37
|
-
import { AuthEmailService } from './auth-email.service';
|
|
38
34
|
import { CompanyBranch } from '../entities/company-branch.entity';
|
|
39
35
|
import { Company } from '../entities/company.entity';
|
|
40
36
|
import { UserCompanyPermission } from '../entities/user-company-permission.entity';
|
|
41
37
|
import { User } from '../entities/user.entity';
|
|
38
|
+
import { PermissionType } from '../enums';
|
|
39
|
+
import { AuthConfigService } from './auth-config.service';
|
|
40
|
+
import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
41
|
+
import { AuthEmailService } from './auth-email.service';
|
|
42
42
|
import { CompanySelectionSessionService } from './company-selection-session.service';
|
|
43
43
|
export class AuthenticationService {
|
|
44
|
+
// Feature Flags
|
|
45
|
+
/** Check if company feature is enabled (tenant-aware with global fallback) */ isCompanyFeatureEnabled() {
|
|
46
|
+
return this.dataSourceProvider?.isCompanyFeatureEnabled() ?? this.authConfig.isCompanyFeatureEnabled();
|
|
47
|
+
}
|
|
44
48
|
// Repository Getters
|
|
45
49
|
async getUserRepository() {
|
|
46
50
|
if (!this.dataSourceProvider) {
|
|
@@ -49,24 +53,31 @@ export class AuthenticationService {
|
|
|
49
53
|
return this.dataSourceProvider.getRepository(User);
|
|
50
54
|
}
|
|
51
55
|
async getPermissionRepository() {
|
|
52
|
-
if (!this.
|
|
56
|
+
if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
|
|
53
57
|
return this.dataSourceProvider.getRepository(UserCompanyPermission);
|
|
54
58
|
}
|
|
55
59
|
async getCompanyRepository() {
|
|
56
|
-
if (!this.
|
|
60
|
+
if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
|
|
57
61
|
return this.dataSourceProvider.getRepository(Company);
|
|
58
62
|
}
|
|
59
63
|
async getBranchRepository() {
|
|
60
|
-
if (!this.
|
|
64
|
+
if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
|
|
61
65
|
return this.dataSourceProvider.getRepository(CompanyBranch);
|
|
62
66
|
}
|
|
63
67
|
// Login
|
|
64
|
-
|
|
68
|
+
async login(email, password) {
|
|
69
|
+
// Check for pending registration first (user registered but hasn't verified email yet)
|
|
70
|
+
if (await this.authEmailService?.hasPendingRegistration(email)) {
|
|
71
|
+
return {
|
|
72
|
+
requiresEmailVerification: true,
|
|
73
|
+
email,
|
|
74
|
+
message: 'Please verify your email address to complete registration.'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
65
77
|
const user = await this.validateUser(email, password);
|
|
66
78
|
if (!user.isActive) {
|
|
67
79
|
throw new UnauthorizedException('User account is deactivated');
|
|
68
80
|
}
|
|
69
|
-
// Check email verification if email service is enabled
|
|
70
81
|
if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
|
|
71
82
|
return {
|
|
72
83
|
requiresEmailVerification: true,
|
|
@@ -74,7 +85,7 @@ export class AuthenticationService {
|
|
|
74
85
|
message: 'Please verify your email address before logging in.'
|
|
75
86
|
};
|
|
76
87
|
}
|
|
77
|
-
if (!this.
|
|
88
|
+
if (!this.isCompanyFeatureEnabled()) {
|
|
78
89
|
return this.generateLoginResponse(user);
|
|
79
90
|
}
|
|
80
91
|
return this.handleCompanyBasedLogin(user);
|
|
@@ -106,7 +117,7 @@ export class AuthenticationService {
|
|
|
106
117
|
// If user has no company assignments yet, return simple login response
|
|
107
118
|
// This allows users to login and be assigned to companies later
|
|
108
119
|
if (companies.length === 0) {
|
|
109
|
-
return this.generateLoginResponse(user
|
|
120
|
+
return this.generateLoginResponse(user);
|
|
110
121
|
}
|
|
111
122
|
// Get branch permissions
|
|
112
123
|
const branchPermissions = await permissionRepository.find({
|
|
@@ -137,12 +148,17 @@ export class AuthenticationService {
|
|
|
137
148
|
email: dto.email
|
|
138
149
|
}
|
|
139
150
|
});
|
|
151
|
+
// When company feature is enabled and user exists, allow joining/creating company
|
|
152
|
+
if (existingUser && this.isCompanyFeatureEnabled()) {
|
|
153
|
+
return this.handleExistingUserCompanyRegistration(existingUser, dto);
|
|
154
|
+
}
|
|
155
|
+
// User exists but company feature is off - email must be unique
|
|
140
156
|
if (existingUser) {
|
|
141
|
-
throw new BadRequestException('Registration failed. Please try again or contact support.');
|
|
157
|
+
throw new BadRequestException('Registration failed due to email exists. Please try again or contact support.');
|
|
142
158
|
}
|
|
143
159
|
// If email verification is enabled, create pending registration instead of user
|
|
144
160
|
if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
|
|
145
|
-
const companyData = this.
|
|
161
|
+
const companyData = this.isCompanyFeatureEnabled() ? {
|
|
146
162
|
companySlug: dto.companySlug,
|
|
147
163
|
branchSlug: dto.branchSlug,
|
|
148
164
|
newCompanyName: dto.newCompanyName,
|
|
@@ -165,6 +181,38 @@ export class AuthenticationService {
|
|
|
165
181
|
// No email verification - create user directly
|
|
166
182
|
return this.createUserDirectly(dto);
|
|
167
183
|
}
|
|
184
|
+
/** Handle registration for existing user when company feature is enabled */ async handleExistingUserCompanyRegistration(existingUser, dto) {
|
|
185
|
+
if (!dto.companySlug && !dto.newCompanyName) {
|
|
186
|
+
throw new BadRequestException('Email already registered. Please login or provide a company to join/create.');
|
|
187
|
+
}
|
|
188
|
+
const permissionRepository = await this.getPermissionRepository();
|
|
189
|
+
const companyRepository = await this.getCompanyRepository();
|
|
190
|
+
if (!permissionRepository || !companyRepository) {
|
|
191
|
+
throw new BadRequestException('Company feature repositories not available');
|
|
192
|
+
}
|
|
193
|
+
// For existing users joining a company, check if already assigned
|
|
194
|
+
if (dto.companySlug) {
|
|
195
|
+
const company = await companyRepository.findOne({
|
|
196
|
+
where: {
|
|
197
|
+
slug: dto.companySlug,
|
|
198
|
+
isActive: true
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
if (company) {
|
|
202
|
+
const existingPermission = await permissionRepository.findOne({
|
|
203
|
+
where: {
|
|
204
|
+
userId: existingUser.id,
|
|
205
|
+
permissionType: PermissionType.COMPANY,
|
|
206
|
+
targetId: company.id
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
if (existingPermission) {
|
|
210
|
+
throw new BadRequestException('You are already assigned to this company. Please login.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return this.processCompanyRegistration(existingUser, dto, true);
|
|
215
|
+
}
|
|
168
216
|
/** Create user directly (no email verification) */ async createUserDirectly(dto) {
|
|
169
217
|
const userRepository = await this.getUserRepository();
|
|
170
218
|
const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
@@ -178,14 +226,13 @@ export class AuthenticationService {
|
|
|
178
226
|
phoneVerified: false
|
|
179
227
|
});
|
|
180
228
|
await userRepository.save(user);
|
|
181
|
-
if (!this.
|
|
182
|
-
return this.generateRegistrationResponse(user
|
|
229
|
+
if (!this.isCompanyFeatureEnabled()) {
|
|
230
|
+
return this.generateRegistrationResponse(user);
|
|
183
231
|
}
|
|
184
|
-
return this.
|
|
232
|
+
return this.processCompanyRegistration(user, dto);
|
|
185
233
|
}
|
|
186
234
|
/** Create user from pending registration (after email verification) */ async createUserFromPendingRegistration(pending) {
|
|
187
235
|
const userRepository = await this.getUserRepository();
|
|
188
|
-
// Check if user already exists (race condition protection)
|
|
189
236
|
const existingUser = await userRepository.findOne({
|
|
190
237
|
where: {
|
|
191
238
|
email: pending.email
|
|
@@ -194,7 +241,6 @@ export class AuthenticationService {
|
|
|
194
241
|
if (existingUser) {
|
|
195
242
|
throw new BadRequestException('This email has already been registered.');
|
|
196
243
|
}
|
|
197
|
-
// Create user with verified email
|
|
198
244
|
const user = userRepository.create({
|
|
199
245
|
name: pending.name,
|
|
200
246
|
email: pending.email,
|
|
@@ -205,107 +251,103 @@ export class AuthenticationService {
|
|
|
205
251
|
phoneVerified: false
|
|
206
252
|
});
|
|
207
253
|
await userRepository.save(user);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return this.handleCompanyRegistrationFromPending(user, pending.companyData);
|
|
254
|
+
if (this.isCompanyFeatureEnabled() && pending.companyData) {
|
|
255
|
+
return this.processCompanyRegistration(user, pending.companyData);
|
|
211
256
|
}
|
|
212
|
-
return this.generateRegistrationResponse(user
|
|
213
|
-
}
|
|
214
|
-
/** Handle company registration from pending registration data */ async handleCompanyRegistrationFromPending(user, companyData) {
|
|
215
|
-
return this.processCompanyRegistration(user, companyData, false);
|
|
257
|
+
return this.generateRegistrationResponse(user);
|
|
216
258
|
}
|
|
217
|
-
/**
|
|
218
|
-
return this.processCompanyRegistration(user, dto, requiresEmailVerification);
|
|
219
|
-
}
|
|
220
|
-
/** Shared logic for company registration (join existing or create new) */ async processCompanyRegistration(user, data, requiresEmailVerification) {
|
|
259
|
+
/** Process company registration (join existing or create new) */ async processCompanyRegistration(user, data, throwOnUnavailable = false) {
|
|
221
260
|
const companyRepository = await this.getCompanyRepository();
|
|
222
261
|
const branchRepository = await this.getBranchRepository();
|
|
223
262
|
const permissionRepository = await this.getPermissionRepository();
|
|
224
263
|
if (!companyRepository || !branchRepository || !permissionRepository) {
|
|
225
|
-
|
|
264
|
+
if (throwOnUnavailable) throw new BadRequestException('Company feature repositories not available');
|
|
265
|
+
return this.generateRegistrationResponse(user);
|
|
266
|
+
}
|
|
267
|
+
const { company, branch } = await this.findOrCreateCompanyAndBranch(data, companyRepository, branchRepository);
|
|
268
|
+
if (company) {
|
|
269
|
+
await this.assignUserPermissions(user.id, company.id, branch?.id, permissionRepository);
|
|
270
|
+
return this.generateRegistrationResponseWithCompany(user, company, branch ?? undefined);
|
|
226
271
|
}
|
|
227
|
-
|
|
228
|
-
|
|
272
|
+
return this.generateRegistrationResponse(user);
|
|
273
|
+
}
|
|
274
|
+
/** Find existing company/branch or create new ones */ async findOrCreateCompanyAndBranch(data, companyRepo, branchRepo) {
|
|
229
275
|
if (data.companySlug) {
|
|
230
|
-
company = await
|
|
276
|
+
const company = await companyRepo.findOne({
|
|
231
277
|
where: {
|
|
232
278
|
slug: data.companySlug,
|
|
233
279
|
isActive: true
|
|
234
280
|
}
|
|
235
281
|
});
|
|
236
|
-
if (!company)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
slug: data.branchSlug,
|
|
243
|
-
companyId: company.id,
|
|
244
|
-
isActive: true
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
if (!branch) {
|
|
248
|
-
throw new BadRequestException('Branch not found or inactive');
|
|
282
|
+
if (!company) throw new BadRequestException('Company not found or inactive');
|
|
283
|
+
const branch = data.branchSlug ? await branchRepo.findOne({
|
|
284
|
+
where: {
|
|
285
|
+
slug: data.branchSlug,
|
|
286
|
+
companyId: company.id,
|
|
287
|
+
isActive: true
|
|
249
288
|
}
|
|
250
|
-
}
|
|
251
|
-
branch = await branchRepository.findOne({
|
|
252
|
-
where: {
|
|
253
|
-
companyId: company.id,
|
|
254
|
-
isActive: true
|
|
255
|
-
},
|
|
256
|
-
order: {
|
|
257
|
-
serial: 'ASC',
|
|
258
|
-
createdAt: 'ASC'
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
} else if (data.newCompanyName) {
|
|
263
|
-
const slug = this.generateSlug(data.newCompanyName);
|
|
264
|
-
const existingCompany = await companyRepository.findOne({
|
|
289
|
+
}) : await branchRepo.findOne({
|
|
265
290
|
where: {
|
|
266
|
-
|
|
291
|
+
companyId: company.id,
|
|
292
|
+
isActive: true
|
|
293
|
+
},
|
|
294
|
+
order: {
|
|
295
|
+
serial: 'ASC',
|
|
296
|
+
createdAt: 'ASC'
|
|
267
297
|
}
|
|
268
298
|
});
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
299
|
+
if (data.branchSlug && !branch) throw new BadRequestException('Branch not found or inactive');
|
|
300
|
+
return {
|
|
301
|
+
company,
|
|
302
|
+
branch
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (data.newCompanyName) {
|
|
306
|
+
const slug = await generateUniqueSlug(data.newCompanyName, async (baseSlug)=>{
|
|
307
|
+
const rows = await companyRepo.createQueryBuilder('c').select('c.slug').where('c.slug = :base OR c.slug LIKE :pattern', {
|
|
308
|
+
base: baseSlug,
|
|
309
|
+
pattern: `${baseSlug}-%`
|
|
310
|
+
}).getMany();
|
|
311
|
+
return rows.map((r)=>r.slug);
|
|
312
|
+
});
|
|
313
|
+
const company = companyRepo.create({
|
|
273
314
|
name: data.newCompanyName,
|
|
274
315
|
slug,
|
|
275
316
|
phone: data.newCompanyPhone,
|
|
276
317
|
address: data.newCompanyAddress,
|
|
277
318
|
isActive: true
|
|
278
319
|
});
|
|
279
|
-
await
|
|
280
|
-
branch =
|
|
320
|
+
await companyRepo.save(company);
|
|
321
|
+
const branch = branchRepo.create({
|
|
281
322
|
name: 'Main Branch',
|
|
282
323
|
slug: 'main-branch',
|
|
283
324
|
companyId: company.id,
|
|
284
325
|
isActive: true
|
|
285
326
|
});
|
|
286
|
-
await
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
permissionType: PermissionType.COMPANY,
|
|
292
|
-
targetId: company.id
|
|
293
|
-
});
|
|
294
|
-
await permissionRepository.save(companyPermission);
|
|
295
|
-
if (branch) {
|
|
296
|
-
const branchPermission = permissionRepository.create({
|
|
297
|
-
userId: user.id,
|
|
298
|
-
permissionType: PermissionType.BRANCH,
|
|
299
|
-
targetId: branch.id
|
|
300
|
-
});
|
|
301
|
-
await permissionRepository.save(branchPermission);
|
|
302
|
-
}
|
|
303
|
-
return this.generateRegistrationResponseWithCompany(user, company, branch ?? undefined, requiresEmailVerification);
|
|
327
|
+
await branchRepo.save(branch);
|
|
328
|
+
return {
|
|
329
|
+
company,
|
|
330
|
+
branch
|
|
331
|
+
};
|
|
304
332
|
}
|
|
305
|
-
return
|
|
333
|
+
return {
|
|
334
|
+
company: null,
|
|
335
|
+
branch: null
|
|
336
|
+
};
|
|
306
337
|
}
|
|
307
|
-
/**
|
|
308
|
-
|
|
338
|
+
/** Assign user to company and optionally branch */ async assignUserPermissions(userId, companyId, branchId, permissionRepo) {
|
|
339
|
+
await permissionRepo.save(permissionRepo.create({
|
|
340
|
+
userId,
|
|
341
|
+
permissionType: PermissionType.COMPANY,
|
|
342
|
+
targetId: companyId
|
|
343
|
+
}));
|
|
344
|
+
if (branchId) {
|
|
345
|
+
await permissionRepo.save(permissionRepo.create({
|
|
346
|
+
userId,
|
|
347
|
+
permissionType: PermissionType.BRANCH,
|
|
348
|
+
targetId: branchId
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
309
351
|
}
|
|
310
352
|
// Company/Branch Selection
|
|
311
353
|
/** Select company and branch (step 2 of login). Frontend receives all options and user selects both. */ async select(sessionId, companyId, branchId) {
|
|
@@ -317,66 +359,13 @@ export class AuthenticationService {
|
|
|
317
359
|
if (!session) {
|
|
318
360
|
throw new UnauthorizedException('Invalid or expired session');
|
|
319
361
|
}
|
|
320
|
-
|
|
321
|
-
const user = await userRepository.findOne({
|
|
322
|
-
where: {
|
|
323
|
-
id: session.userId
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
if (!user) {
|
|
327
|
-
throw new NotFoundException('User not found');
|
|
328
|
-
}
|
|
329
|
-
const permissionRepository = await this.getPermissionRepository();
|
|
330
|
-
const companyRepository = await this.getCompanyRepository();
|
|
331
|
-
const branchRepository = await this.getBranchRepository();
|
|
332
|
-
if (!permissionRepository || !companyRepository || !branchRepository) {
|
|
333
|
-
throw new BadRequestException('Repositories not available');
|
|
334
|
-
}
|
|
335
|
-
// Validate company access
|
|
336
|
-
const companyPermission = await permissionRepository.findOne({
|
|
337
|
-
where: {
|
|
338
|
-
userId: user.id,
|
|
339
|
-
permissionType: PermissionType.COMPANY,
|
|
340
|
-
targetId: companyId
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
if (!companyPermission) {
|
|
344
|
-
throw new UnauthorizedException('No access to this company');
|
|
345
|
-
}
|
|
346
|
-
const company = await companyRepository.findOne({
|
|
347
|
-
where: {
|
|
348
|
-
id: companyId,
|
|
349
|
-
isActive: true
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
if (!company) {
|
|
353
|
-
throw new UnauthorizedException('Company not found or inactive');
|
|
354
|
-
}
|
|
355
|
-
// Validate branch access
|
|
356
|
-
const branchPermission = await permissionRepository.findOne({
|
|
357
|
-
where: {
|
|
358
|
-
userId: user.id,
|
|
359
|
-
permissionType: PermissionType.BRANCH,
|
|
360
|
-
targetId: branchId
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
if (!branchPermission) {
|
|
364
|
-
throw new UnauthorizedException('No access to this branch');
|
|
365
|
-
}
|
|
366
|
-
const branch = await branchRepository.findOne({
|
|
367
|
-
where: {
|
|
368
|
-
id: branchId,
|
|
369
|
-
companyId,
|
|
370
|
-
isActive: true
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
if (!branch) {
|
|
374
|
-
throw new UnauthorizedException('Branch not found or inactive');
|
|
375
|
-
}
|
|
376
|
-
return this.generateLoginResponseWithCompany(user, company, branch);
|
|
362
|
+
return this.validateAndLoginWithCompany(session.userId, companyId, branchId);
|
|
377
363
|
}
|
|
378
364
|
/** Switch company/branch after login */ async switchCompany(userId, companyId, branchId) {
|
|
379
365
|
this.ensureCompanyFeatureEnabled();
|
|
366
|
+
return this.validateAndLoginWithCompany(userId, companyId, branchId);
|
|
367
|
+
}
|
|
368
|
+
/** Shared: Validate user access to company/branch and generate login response */ async validateAndLoginWithCompany(userId, companyId, branchId) {
|
|
380
369
|
const userRepository = await this.getUserRepository();
|
|
381
370
|
const user = await userRepository.findOne({
|
|
382
371
|
where: {
|
|
@@ -393,14 +382,14 @@ export class AuthenticationService {
|
|
|
393
382
|
throw new BadRequestException('Repositories not available');
|
|
394
383
|
}
|
|
395
384
|
// Validate company access
|
|
396
|
-
const
|
|
385
|
+
const hasCompanyAccess = await permissionRepository.findOne({
|
|
397
386
|
where: {
|
|
398
387
|
userId,
|
|
399
388
|
permissionType: PermissionType.COMPANY,
|
|
400
389
|
targetId: companyId
|
|
401
390
|
}
|
|
402
391
|
});
|
|
403
|
-
if (!
|
|
392
|
+
if (!hasCompanyAccess) {
|
|
404
393
|
throw new UnauthorizedException('No access to this company');
|
|
405
394
|
}
|
|
406
395
|
const company = await companyRepository.findOne({
|
|
@@ -412,14 +401,15 @@ export class AuthenticationService {
|
|
|
412
401
|
if (!company) {
|
|
413
402
|
throw new UnauthorizedException('Company not found or inactive');
|
|
414
403
|
}
|
|
415
|
-
|
|
404
|
+
// Validate branch access
|
|
405
|
+
const hasBranchAccess = await permissionRepository.findOne({
|
|
416
406
|
where: {
|
|
417
407
|
userId,
|
|
418
408
|
permissionType: PermissionType.BRANCH,
|
|
419
409
|
targetId: branchId
|
|
420
410
|
}
|
|
421
411
|
});
|
|
422
|
-
if (!
|
|
412
|
+
if (!hasBranchAccess) {
|
|
423
413
|
throw new UnauthorizedException('No access to this branch');
|
|
424
414
|
}
|
|
425
415
|
const branch = await branchRepository.findOne({
|
|
@@ -440,9 +430,7 @@ export class AuthenticationService {
|
|
|
440
430
|
const payload = this.jwtService.verify(refreshToken, {
|
|
441
431
|
secret: this.authConfig.getRefreshTokenSecret()
|
|
442
432
|
});
|
|
443
|
-
|
|
444
|
-
const isRevoked = await this.isTokenRevoked(payload.id, payload.iat);
|
|
445
|
-
if (isRevoked) {
|
|
433
|
+
if (await this.isTokenRevoked(payload.id, payload.iat)) {
|
|
446
434
|
throw new UnauthorizedException('Token has been revoked');
|
|
447
435
|
}
|
|
448
436
|
const userRepository = await this.getUserRepository();
|
|
@@ -454,7 +442,7 @@ export class AuthenticationService {
|
|
|
454
442
|
if (!user || !user.isActive) {
|
|
455
443
|
throw new UnauthorizedException('Invalid refresh token');
|
|
456
444
|
}
|
|
457
|
-
|
|
445
|
+
return this.generateTokens({
|
|
458
446
|
id: user.id,
|
|
459
447
|
email: user.email,
|
|
460
448
|
type: 'access',
|
|
@@ -464,44 +452,9 @@ export class AuthenticationService {
|
|
|
464
452
|
},
|
|
465
453
|
...payload.branchId && {
|
|
466
454
|
branchId: payload.branchId
|
|
467
|
-
},
|
|
468
|
-
...payload.companyLogoId && {
|
|
469
|
-
companyLogoId: payload.companyLogoId
|
|
470
|
-
},
|
|
471
|
-
...payload.branchLogoId && {
|
|
472
|
-
branchLogoId: payload.branchLogoId
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
const refreshPayload = {
|
|
476
|
-
id: user.id,
|
|
477
|
-
email: user.email,
|
|
478
|
-
type: 'refresh',
|
|
479
|
-
profilePictureId: user.profilePictureId ?? undefined,
|
|
480
|
-
...payload.companyId && {
|
|
481
|
-
companyId: payload.companyId
|
|
482
|
-
},
|
|
483
|
-
...payload.branchId && {
|
|
484
|
-
branchId: payload.branchId
|
|
485
|
-
},
|
|
486
|
-
...payload.companyLogoId && {
|
|
487
|
-
companyLogoId: payload.companyLogoId
|
|
488
|
-
},
|
|
489
|
-
...payload.branchLogoId && {
|
|
490
|
-
branchLogoId: payload.branchLogoId
|
|
491
455
|
}
|
|
492
|
-
};
|
|
493
|
-
return {
|
|
494
|
-
accessToken: this.jwtService.sign(accessPayload, {
|
|
495
|
-
secret: this.authConfig.getJwtSecret(),
|
|
496
|
-
expiresIn: this.authConfig.getJwtExpiration()
|
|
497
|
-
}),
|
|
498
|
-
refreshToken: this.jwtService.sign(refreshPayload, {
|
|
499
|
-
secret: this.authConfig.getRefreshTokenSecret(),
|
|
500
|
-
expiresIn: this.authConfig.getRefreshTokenExpiration()
|
|
501
|
-
})
|
|
502
|
-
};
|
|
456
|
+
});
|
|
503
457
|
} catch (error) {
|
|
504
|
-
// Log error details for debugging (not exposed to client)
|
|
505
458
|
ErrorHandler.logError(this.logger, error, 'refreshToken');
|
|
506
459
|
throw new UnauthorizedException('Invalid or expired refresh token');
|
|
507
460
|
}
|
|
@@ -513,11 +466,8 @@ export class AuthenticationService {
|
|
|
513
466
|
id: userId
|
|
514
467
|
}
|
|
515
468
|
});
|
|
516
|
-
if (!user)
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
const isPasswordValid = await bcrypt.compare(currentPassword, user.password || '');
|
|
520
|
-
if (!isPasswordValid) {
|
|
469
|
+
if (!user) throw new UnauthorizedException('User not found');
|
|
470
|
+
if (!await bcrypt.compare(currentPassword, user.password || '')) {
|
|
521
471
|
throw new UnauthorizedException('Current password is incorrect');
|
|
522
472
|
}
|
|
523
473
|
user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
|
|
@@ -653,54 +603,28 @@ export class AuthenticationService {
|
|
|
653
603
|
id: userId
|
|
654
604
|
}
|
|
655
605
|
});
|
|
656
|
-
if (!user)
|
|
657
|
-
throw new UnauthorizedException('User not found');
|
|
658
|
-
}
|
|
659
|
-
// Build response (no tokens)
|
|
606
|
+
if (!user) throw new UnauthorizedException('User not found');
|
|
660
607
|
const response = {
|
|
661
|
-
user:
|
|
662
|
-
id: user.id,
|
|
663
|
-
email: user.email,
|
|
664
|
-
name: user.name ?? undefined,
|
|
665
|
-
phone: user.phone ?? undefined,
|
|
666
|
-
profilePictureId: user.profilePictureId ?? undefined
|
|
667
|
-
}
|
|
608
|
+
user: this.toUserInfo(user)
|
|
668
609
|
};
|
|
669
|
-
|
|
670
|
-
if (this.authConfig.isCompanyFeatureEnabled()) {
|
|
671
|
-
// When company feature is enabled, companyId MUST be present
|
|
610
|
+
if (this.isCompanyFeatureEnabled()) {
|
|
672
611
|
if (!companyId) {
|
|
673
|
-
throw new UnauthorizedException('Company feature is enabled but user has no company assigned.
|
|
612
|
+
throw new UnauthorizedException('Company feature is enabled but user has no company assigned.');
|
|
674
613
|
}
|
|
675
614
|
const company = await this.getCompanyById(companyId);
|
|
676
|
-
if (!company)
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
response.company = {
|
|
680
|
-
id: company.id,
|
|
681
|
-
name: company.name,
|
|
682
|
-
slug: company.slug,
|
|
683
|
-
logoId: company.logoId ?? null
|
|
684
|
-
};
|
|
685
|
-
// Branch is optional in some configurations, but if provided it must exist
|
|
615
|
+
if (!company) throw new UnauthorizedException('Company not found.');
|
|
616
|
+
response.company = this.toCompanyInfo(company);
|
|
686
617
|
if (branchId) {
|
|
687
618
|
const branch = await this.getBranchById(branchId);
|
|
688
|
-
if (!branch)
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
response.branch = {
|
|
692
|
-
id: branch.id,
|
|
693
|
-
name: branch.name,
|
|
694
|
-
slug: branch.slug,
|
|
695
|
-
logoId: branch.logoId ?? null
|
|
696
|
-
};
|
|
619
|
+
if (!branch) throw new UnauthorizedException('Branch not found.');
|
|
620
|
+
response.branch = this.toBranchInfo(branch);
|
|
697
621
|
}
|
|
698
622
|
}
|
|
699
623
|
return response;
|
|
700
624
|
}
|
|
701
625
|
// Helper Methods
|
|
702
626
|
ensureCompanyFeatureEnabled() {
|
|
703
|
-
if (!this.
|
|
627
|
+
if (!this.isCompanyFeatureEnabled()) {
|
|
704
628
|
throw new BadRequestException('Company feature is not enabled');
|
|
705
629
|
}
|
|
706
630
|
}
|
|
@@ -711,11 +635,7 @@ export class AuthenticationService {
|
|
|
711
635
|
email
|
|
712
636
|
}
|
|
713
637
|
});
|
|
714
|
-
if (!user) {
|
|
715
|
-
throw new UnauthorizedException('Invalid credentials');
|
|
716
|
-
}
|
|
717
|
-
const isPasswordValid = await bcrypt.compare(password, user.password || '');
|
|
718
|
-
if (!isPasswordValid) {
|
|
638
|
+
if (!user || !await bcrypt.compare(password, user.password || '')) {
|
|
719
639
|
throw new UnauthorizedException('Invalid credentials');
|
|
720
640
|
}
|
|
721
641
|
user.lastLoginAt = new Date();
|
|
@@ -723,84 +643,77 @@ export class AuthenticationService {
|
|
|
723
643
|
return user;
|
|
724
644
|
}
|
|
725
645
|
// Response Generators
|
|
726
|
-
|
|
727
|
-
|
|
646
|
+
/** Create user info object */ toUserInfo(user) {
|
|
647
|
+
return {
|
|
728
648
|
id: user.id,
|
|
729
649
|
email: user.email,
|
|
730
|
-
|
|
650
|
+
name: user.name ?? undefined,
|
|
651
|
+
phone: user.phone ?? null,
|
|
652
|
+
profilePictureId: user.profilePictureId ?? null
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/** Create company info object */ toCompanyInfo(company) {
|
|
656
|
+
return {
|
|
657
|
+
id: company.id,
|
|
658
|
+
name: company.name,
|
|
659
|
+
slug: company.slug,
|
|
660
|
+
logoId: company.logoId ?? null
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/** Create branch info object */ toBranchInfo(branch) {
|
|
664
|
+
return {
|
|
665
|
+
id: branch.id,
|
|
666
|
+
name: branch.name,
|
|
667
|
+
slug: branch.slug,
|
|
668
|
+
logoId: branch.logoId ?? null
|
|
731
669
|
};
|
|
732
|
-
|
|
670
|
+
}
|
|
671
|
+
/** Generate JWT tokens */ generateTokens(payload) {
|
|
672
|
+
const accessToken = this.jwtService.sign({
|
|
673
|
+
...payload,
|
|
674
|
+
type: 'access'
|
|
675
|
+
}, {
|
|
733
676
|
secret: this.authConfig.getJwtSecret(),
|
|
734
677
|
expiresIn: this.authConfig.getJwtExpiration()
|
|
735
678
|
});
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
email: user.email,
|
|
679
|
+
const refreshToken = this.jwtService.sign({
|
|
680
|
+
...payload,
|
|
739
681
|
type: 'refresh'
|
|
740
|
-
}
|
|
741
|
-
const refreshToken = this.jwtService.sign(refreshPayload, {
|
|
682
|
+
}, {
|
|
742
683
|
secret: this.authConfig.getRefreshTokenSecret(),
|
|
743
684
|
expiresIn: this.authConfig.getRefreshTokenExpiration()
|
|
744
685
|
});
|
|
745
686
|
return {
|
|
746
687
|
accessToken,
|
|
747
|
-
refreshToken
|
|
748
|
-
user: {
|
|
749
|
-
id: user.id,
|
|
750
|
-
email: user.email,
|
|
751
|
-
name: user.name ?? undefined,
|
|
752
|
-
phone: user.phone ?? null,
|
|
753
|
-
profilePictureId: user.profilePictureId ?? null
|
|
754
|
-
},
|
|
755
|
-
companyFeatureEnabled
|
|
688
|
+
refreshToken
|
|
756
689
|
};
|
|
757
690
|
}
|
|
758
|
-
|
|
759
|
-
const
|
|
691
|
+
generateLoginResponse(user) {
|
|
692
|
+
const { accessToken, refreshToken } = this.generateTokens({
|
|
760
693
|
id: user.id,
|
|
761
694
|
email: user.email,
|
|
762
|
-
type: 'access'
|
|
763
|
-
companyId: company.id,
|
|
764
|
-
branchId: branch?.id
|
|
765
|
-
};
|
|
766
|
-
const accessToken = this.jwtService.sign(payload, {
|
|
767
|
-
secret: this.authConfig.getJwtSecret(),
|
|
768
|
-
expiresIn: this.authConfig.getJwtExpiration()
|
|
695
|
+
type: 'access'
|
|
769
696
|
});
|
|
770
|
-
|
|
697
|
+
return {
|
|
698
|
+
accessToken,
|
|
699
|
+
refreshToken,
|
|
700
|
+
user: this.toUserInfo(user)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
generateLoginResponseWithCompany(user, company, branch) {
|
|
704
|
+
const { accessToken, refreshToken } = this.generateTokens({
|
|
771
705
|
id: user.id,
|
|
772
706
|
email: user.email,
|
|
773
|
-
type: '
|
|
707
|
+
type: 'access',
|
|
774
708
|
companyId: company.id,
|
|
775
709
|
branchId: branch?.id
|
|
776
|
-
};
|
|
777
|
-
const refreshToken = this.jwtService.sign(refreshPayload, {
|
|
778
|
-
secret: this.authConfig.getRefreshTokenSecret(),
|
|
779
|
-
expiresIn: this.authConfig.getRefreshTokenExpiration()
|
|
780
710
|
});
|
|
781
711
|
return {
|
|
782
712
|
accessToken,
|
|
783
713
|
refreshToken,
|
|
784
|
-
user:
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
name: user.name ?? undefined,
|
|
788
|
-
phone: user.phone ?? null,
|
|
789
|
-
profilePictureId: user.profilePictureId ?? null
|
|
790
|
-
},
|
|
791
|
-
company: {
|
|
792
|
-
id: company.id,
|
|
793
|
-
name: company.name,
|
|
794
|
-
slug: company.slug,
|
|
795
|
-
logoId: company.logoId ?? null
|
|
796
|
-
},
|
|
797
|
-
branch: branch ? {
|
|
798
|
-
id: branch.id,
|
|
799
|
-
name: branch.name,
|
|
800
|
-
slug: branch.slug,
|
|
801
|
-
logoId: branch.logoId ?? null
|
|
802
|
-
} : undefined,
|
|
803
|
-
companyFeatureEnabled: true
|
|
714
|
+
user: this.toUserInfo(user),
|
|
715
|
+
company: this.toCompanyInfo(company),
|
|
716
|
+
branch: branch ? this.toBranchInfo(branch) : undefined
|
|
804
717
|
};
|
|
805
718
|
}
|
|
806
719
|
async generateCompanySelectionResponse(user, companies, branches) {
|
|
@@ -808,88 +721,31 @@ export class AuthenticationService {
|
|
|
808
721
|
throw new BadRequestException('Session service not available');
|
|
809
722
|
}
|
|
810
723
|
const sessionId = await this.sessionService.createSession(user.id);
|
|
811
|
-
// Build companies with their branches
|
|
812
724
|
const companiesWithBranches = companies.map((company)=>({
|
|
813
|
-
|
|
814
|
-
name: company.name,
|
|
815
|
-
slug: company.slug,
|
|
725
|
+
...this.toCompanyInfo(company),
|
|
816
726
|
isActive: company.isActive,
|
|
817
|
-
logoId: company.logoId ?? null,
|
|
818
727
|
branches: branches.filter((b)=>b.companyId === company.id).map((b)=>({
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
slug: b.slug,
|
|
822
|
-
isActive: b.isActive,
|
|
823
|
-
logoId: b.logoId ?? null
|
|
728
|
+
...this.toBranchInfo(b),
|
|
729
|
+
isActive: b.isActive
|
|
824
730
|
}))
|
|
825
731
|
}));
|
|
826
732
|
return {
|
|
827
733
|
requiresSelection: true,
|
|
828
734
|
sessionId,
|
|
829
735
|
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
|
|
830
|
-
user:
|
|
831
|
-
id: user.id,
|
|
832
|
-
email: user.email,
|
|
833
|
-
name: user.name ?? undefined,
|
|
834
|
-
phone: user.phone ?? null,
|
|
835
|
-
profilePictureId: user.profilePictureId ?? null
|
|
836
|
-
},
|
|
736
|
+
user: this.toUserInfo(user),
|
|
837
737
|
companies: companiesWithBranches
|
|
838
738
|
};
|
|
839
739
|
}
|
|
840
|
-
|
|
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);
|
|
740
|
+
generateRegistrationResponse(user) {
|
|
856
741
|
return {
|
|
857
|
-
...
|
|
742
|
+
...this.generateLoginResponse(user),
|
|
858
743
|
requiresEmailVerification: false
|
|
859
744
|
};
|
|
860
745
|
}
|
|
861
|
-
|
|
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);
|
|
746
|
+
generateRegistrationResponseWithCompany(user, company, branch) {
|
|
891
747
|
return {
|
|
892
|
-
...
|
|
748
|
+
...this.generateLoginResponseWithCompany(user, company, branch),
|
|
893
749
|
requiresEmailVerification: false
|
|
894
750
|
};
|
|
895
751
|
}
|