@flusys/nestjs-auth 1.0.0-beta → 1.1.0-beta
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/cjs/config/auth.constants.js +4 -0
- package/cjs/controllers/authentication.controller.js +167 -5
- package/cjs/controllers/user.controller.js +151 -0
- package/cjs/dtos/authentication.dto.js +106 -3
- package/cjs/entities/index.js +5 -1
- package/cjs/entities/pending-registration.entity.js +125 -0
- package/cjs/entities/user-token.entity.js +129 -0
- package/cjs/interfaces/auth-email-provider.interface.js +45 -0
- package/cjs/interfaces/index.js +2 -0
- package/cjs/interfaces/user-enricher.interface.js +4 -0
- package/cjs/modules/auth.module.js +8 -2
- package/cjs/services/auth-config.service.js +4 -0
- package/cjs/services/auth-datasource.provider.js +4 -2
- package/cjs/services/auth-email.service.js +452 -0
- package/cjs/services/authentication.service.js +156 -37
- package/cjs/services/index.js +1 -0
- package/cjs/services/user.service.js +147 -11
- package/config/auth.constants.d.ts +1 -0
- package/controllers/authentication.controller.d.ts +13 -9
- package/controllers/user.controller.d.ts +27 -0
- package/dtos/authentication.dto.d.ts +21 -3
- package/dtos/user.dto.d.ts +1 -0
- package/entities/index.d.ts +7 -3
- package/entities/pending-registration.entity.d.ts +17 -0
- package/entities/user-token.entity.d.ts +15 -0
- package/fesm/config/auth.constants.js +11 -0
- package/fesm/controllers/authentication.controller.js +168 -6
- package/fesm/controllers/user.controller.js +151 -0
- package/fesm/dtos/authentication.dto.js +92 -3
- package/fesm/entities/index.js +7 -1
- package/fesm/entities/pending-registration.entity.js +115 -0
- package/fesm/entities/user-token.entity.js +113 -0
- package/fesm/interfaces/auth-email-provider.interface.js +38 -0
- package/fesm/interfaces/authentication.interface.js +1 -1
- package/fesm/interfaces/index.js +2 -0
- package/fesm/interfaces/user-enricher.interface.js +26 -0
- package/fesm/modules/auth.module.js +9 -3
- package/fesm/services/auth-config.service.js +4 -0
- package/fesm/services/auth-datasource.provider.js +4 -2
- package/fesm/services/auth-email.service.js +401 -0
- package/fesm/services/authentication.service.js +156 -37
- package/fesm/services/index.js +1 -0
- package/fesm/services/user.service.js +149 -13
- 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 +67 -0
- package/interfaces/user.interface.d.ts +1 -0
- package/package.json +3 -3
- package/services/auth-config.service.d.ts +1 -0
- package/services/auth-email.service.d.ts +50 -0
- package/services/authentication.service.d.ts +16 -4
- package/services/index.d.ts +1 -0
- package/services/user.service.d.ts +16 -3
|
@@ -36,6 +36,7 @@ import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
|
36
36
|
import { PermissionType } from '../enums';
|
|
37
37
|
import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX } from '../config';
|
|
38
38
|
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
39
|
+
import { AuthEmailService } from './auth-email.service';
|
|
39
40
|
import { CompanyBranch } from '../entities/company-branch.entity';
|
|
40
41
|
import { Company } from '../entities/company.entity';
|
|
41
42
|
import { UserCompanyPermission } from '../entities/user-company-permission.entity';
|
|
@@ -79,7 +80,14 @@ export class AuthenticationService {
|
|
|
79
80
|
if (!user.isActive) {
|
|
80
81
|
throw new UnauthorizedException('User account is deactivated');
|
|
81
82
|
}
|
|
82
|
-
//
|
|
83
|
+
// Check email verification if email service is enabled
|
|
84
|
+
if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
|
|
85
|
+
return {
|
|
86
|
+
requiresEmailVerification: true,
|
|
87
|
+
email: user.email,
|
|
88
|
+
message: 'Please verify your email address before logging in.'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
83
91
|
if (!this.authConfig.isCompanyFeatureEnabled()) {
|
|
84
92
|
return this.generateLoginResponse(user);
|
|
85
93
|
}
|
|
@@ -136,10 +144,8 @@ export class AuthenticationService {
|
|
|
136
144
|
return await this.generateCompanySelectionResponse(user, companies, branches);
|
|
137
145
|
}
|
|
138
146
|
// Registration
|
|
139
|
-
/** Register a new user. If
|
|
147
|
+
/** Register a new user. If email enabled, creates pending registration. Otherwise creates user directly. */ async register(dto, verifyUrlBase) {
|
|
140
148
|
const userRepository = await this.getUserRepository();
|
|
141
|
-
// Check if user exists
|
|
142
|
-
// Note: Generic error message to prevent email enumeration attacks
|
|
143
149
|
const existingUser = await userRepository.findOne({
|
|
144
150
|
where: {
|
|
145
151
|
email: dto.email
|
|
@@ -148,7 +154,33 @@ export class AuthenticationService {
|
|
|
148
154
|
if (existingUser) {
|
|
149
155
|
throw new BadRequestException('Registration failed. Please try again or contact support.');
|
|
150
156
|
}
|
|
151
|
-
//
|
|
157
|
+
// If email verification is enabled, create pending registration instead of user
|
|
158
|
+
if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
|
|
159
|
+
const companyData = this.authConfig.isCompanyFeatureEnabled() ? {
|
|
160
|
+
companySlug: dto.companySlug,
|
|
161
|
+
branchSlug: dto.branchSlug,
|
|
162
|
+
newCompanyName: dto.newCompanyName,
|
|
163
|
+
newCompanyPhone: dto.newCompanyPhone,
|
|
164
|
+
newCompanyAddress: dto.newCompanyAddress
|
|
165
|
+
} : undefined;
|
|
166
|
+
const result = await this.authEmailService.createPendingRegistration({
|
|
167
|
+
email: dto.email,
|
|
168
|
+
name: dto.name,
|
|
169
|
+
phone: dto.phone,
|
|
170
|
+
password: dto.password,
|
|
171
|
+
companyData,
|
|
172
|
+
verifyUrlBase
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
requiresEmailVerification: true,
|
|
176
|
+
message: result.message
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// No email verification - create user directly
|
|
180
|
+
return this.createUserDirectly(dto);
|
|
181
|
+
}
|
|
182
|
+
/** Create user directly (no email verification) */ async createUserDirectly(dto) {
|
|
183
|
+
const userRepository = await this.getUserRepository();
|
|
152
184
|
const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
153
185
|
const user = userRepository.create({
|
|
154
186
|
name: dto.name,
|
|
@@ -160,38 +192,68 @@ export class AuthenticationService {
|
|
|
160
192
|
phoneVerified: false
|
|
161
193
|
});
|
|
162
194
|
await userRepository.save(user);
|
|
163
|
-
// If company feature not enabled, return simple response
|
|
164
195
|
if (!this.authConfig.isCompanyFeatureEnabled()) {
|
|
165
|
-
return this.
|
|
196
|
+
return this.generateRegistrationResponse(user, false, false);
|
|
197
|
+
}
|
|
198
|
+
return this.handleCompanyRegistration(user, dto, false);
|
|
199
|
+
}
|
|
200
|
+
/** Create user from pending registration (after email verification) */ async createUserFromPendingRegistration(pending) {
|
|
201
|
+
const userRepository = await this.getUserRepository();
|
|
202
|
+
// Check if user already exists (race condition protection)
|
|
203
|
+
const existingUser = await userRepository.findOne({
|
|
204
|
+
where: {
|
|
205
|
+
email: pending.email
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
if (existingUser) {
|
|
209
|
+
throw new BadRequestException('This email has already been registered.');
|
|
210
|
+
}
|
|
211
|
+
// Create user with verified email
|
|
212
|
+
const user = userRepository.create({
|
|
213
|
+
name: pending.name,
|
|
214
|
+
email: pending.email,
|
|
215
|
+
phone: pending.phone,
|
|
216
|
+
password: pending.passwordHash,
|
|
217
|
+
isActive: true,
|
|
218
|
+
emailVerified: true,
|
|
219
|
+
phoneVerified: false
|
|
220
|
+
});
|
|
221
|
+
await userRepository.save(user);
|
|
222
|
+
// Handle company feature if enabled
|
|
223
|
+
if (this.authConfig.isCompanyFeatureEnabled() && pending.companyData) {
|
|
224
|
+
return this.handleCompanyRegistrationFromPending(user, pending.companyData);
|
|
166
225
|
}
|
|
167
|
-
|
|
168
|
-
|
|
226
|
+
return this.generateRegistrationResponse(user, this.authConfig.isCompanyFeatureEnabled(), false);
|
|
227
|
+
}
|
|
228
|
+
/** Handle company registration from pending registration data */ async handleCompanyRegistrationFromPending(user, companyData) {
|
|
229
|
+
return this.processCompanyRegistration(user, companyData, false);
|
|
230
|
+
}
|
|
231
|
+
/** Handle company-related registration logic */ async handleCompanyRegistration(user, dto, requiresEmailVerification) {
|
|
232
|
+
return this.processCompanyRegistration(user, dto, requiresEmailVerification);
|
|
169
233
|
}
|
|
170
|
-
/**
|
|
234
|
+
/** Shared logic for company registration (join existing or create new) */ async processCompanyRegistration(user, data, requiresEmailVerification) {
|
|
171
235
|
const companyRepository = await this.getCompanyRepository();
|
|
172
236
|
const branchRepository = await this.getBranchRepository();
|
|
173
237
|
const permissionRepository = await this.getPermissionRepository();
|
|
174
238
|
if (!companyRepository || !branchRepository || !permissionRepository) {
|
|
175
|
-
return this.
|
|
239
|
+
return this.generateRegistrationResponse(user, true, requiresEmailVerification);
|
|
176
240
|
}
|
|
177
241
|
let company = null;
|
|
178
242
|
let branch = null;
|
|
179
|
-
|
|
180
|
-
if (dto.companySlug) {
|
|
243
|
+
if (data.companySlug) {
|
|
181
244
|
company = await companyRepository.findOne({
|
|
182
245
|
where: {
|
|
183
|
-
slug:
|
|
246
|
+
slug: data.companySlug,
|
|
184
247
|
isActive: true
|
|
185
248
|
}
|
|
186
249
|
});
|
|
187
250
|
if (!company) {
|
|
188
251
|
throw new BadRequestException('Company not found or inactive');
|
|
189
252
|
}
|
|
190
|
-
|
|
191
|
-
if (dto.branchSlug) {
|
|
253
|
+
if (data.branchSlug) {
|
|
192
254
|
branch = await branchRepository.findOne({
|
|
193
255
|
where: {
|
|
194
|
-
slug:
|
|
256
|
+
slug: data.branchSlug,
|
|
195
257
|
companyId: company.id,
|
|
196
258
|
isActive: true
|
|
197
259
|
}
|
|
@@ -200,7 +262,6 @@ export class AuthenticationService {
|
|
|
200
262
|
throw new BadRequestException('Branch not found or inactive');
|
|
201
263
|
}
|
|
202
264
|
} else {
|
|
203
|
-
// Get first active branch
|
|
204
265
|
branch = await branchRepository.findOne({
|
|
205
266
|
where: {
|
|
206
267
|
companyId: company.id,
|
|
@@ -212,9 +273,8 @@ export class AuthenticationService {
|
|
|
212
273
|
}
|
|
213
274
|
});
|
|
214
275
|
}
|
|
215
|
-
} else if (
|
|
216
|
-
const slug = this.generateSlug(
|
|
217
|
-
// Check if slug exists
|
|
276
|
+
} else if (data.newCompanyName) {
|
|
277
|
+
const slug = this.generateSlug(data.newCompanyName);
|
|
218
278
|
const existingCompany = await companyRepository.findOne({
|
|
219
279
|
where: {
|
|
220
280
|
slug
|
|
@@ -223,16 +283,14 @@ export class AuthenticationService {
|
|
|
223
283
|
if (existingCompany) {
|
|
224
284
|
throw new BadRequestException('Company creation failed. Please try a different name.');
|
|
225
285
|
}
|
|
226
|
-
// Create company
|
|
227
286
|
company = companyRepository.create({
|
|
228
|
-
name:
|
|
287
|
+
name: data.newCompanyName,
|
|
229
288
|
slug,
|
|
230
|
-
phone:
|
|
231
|
-
address:
|
|
289
|
+
phone: data.newCompanyPhone,
|
|
290
|
+
address: data.newCompanyAddress,
|
|
232
291
|
isActive: true
|
|
233
292
|
});
|
|
234
293
|
await companyRepository.save(company);
|
|
235
|
-
// Create default branch
|
|
236
294
|
branch = branchRepository.create({
|
|
237
295
|
name: 'Main Branch',
|
|
238
296
|
slug: 'main-branch',
|
|
@@ -241,16 +299,13 @@ export class AuthenticationService {
|
|
|
241
299
|
});
|
|
242
300
|
await branchRepository.save(branch);
|
|
243
301
|
}
|
|
244
|
-
// Create permissions if company exists
|
|
245
302
|
if (company) {
|
|
246
|
-
// Create company permission
|
|
247
303
|
const companyPermission = permissionRepository.create({
|
|
248
304
|
userId: user.id,
|
|
249
305
|
permissionType: PermissionType.COMPANY,
|
|
250
306
|
targetId: company.id
|
|
251
307
|
});
|
|
252
308
|
await permissionRepository.save(companyPermission);
|
|
253
|
-
// Create branch permission if branch exists
|
|
254
309
|
if (branch) {
|
|
255
310
|
const branchPermission = permissionRepository.create({
|
|
256
311
|
userId: user.id,
|
|
@@ -259,10 +314,9 @@ export class AuthenticationService {
|
|
|
259
314
|
});
|
|
260
315
|
await permissionRepository.save(branchPermission);
|
|
261
316
|
}
|
|
262
|
-
return this.
|
|
317
|
+
return this.generateRegistrationResponseWithCompany(user, company, branch ?? undefined, requiresEmailVerification);
|
|
263
318
|
}
|
|
264
|
-
|
|
265
|
-
return this.generateLoginResponse(user, true);
|
|
319
|
+
return this.generateRegistrationResponse(user, true, requiresEmailVerification);
|
|
266
320
|
}
|
|
267
321
|
/** Generate URL-friendly slug from name */ generateSlug(name) {
|
|
268
322
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 100);
|
|
@@ -615,6 +669,7 @@ export class AuthenticationService {
|
|
|
615
669
|
id: user.id,
|
|
616
670
|
email: user.email,
|
|
617
671
|
name: user.name ?? undefined,
|
|
672
|
+
phone: user.phone ?? undefined,
|
|
618
673
|
profilePictureId: user.profilePictureId ?? undefined
|
|
619
674
|
}
|
|
620
675
|
};
|
|
@@ -701,6 +756,7 @@ export class AuthenticationService {
|
|
|
701
756
|
id: user.id,
|
|
702
757
|
email: user.email,
|
|
703
758
|
name: user.name ?? undefined,
|
|
759
|
+
phone: user.phone ?? null,
|
|
704
760
|
profilePictureId: user.profilePictureId ?? null
|
|
705
761
|
},
|
|
706
762
|
companyFeatureEnabled
|
|
@@ -736,6 +792,7 @@ export class AuthenticationService {
|
|
|
736
792
|
id: user.id,
|
|
737
793
|
email: user.email,
|
|
738
794
|
name: user.name ?? undefined,
|
|
795
|
+
phone: user.phone ?? null,
|
|
739
796
|
profilePictureId: user.profilePictureId ?? null
|
|
740
797
|
},
|
|
741
798
|
company: {
|
|
@@ -781,18 +838,76 @@ export class AuthenticationService {
|
|
|
781
838
|
id: user.id,
|
|
782
839
|
email: user.email,
|
|
783
840
|
name: user.name ?? undefined,
|
|
841
|
+
phone: user.phone ?? null,
|
|
784
842
|
profilePictureId: user.profilePictureId ?? null
|
|
785
843
|
},
|
|
786
844
|
companies: companiesWithBranches
|
|
787
845
|
};
|
|
788
846
|
}
|
|
847
|
+
/** Generate registration response - tokens included only if email verification not required */ generateRegistrationResponse(user, companyFeatureEnabled, requiresEmailVerification) {
|
|
848
|
+
const userInfo = {
|
|
849
|
+
id: user.id,
|
|
850
|
+
email: user.email,
|
|
851
|
+
name: user.name ?? undefined,
|
|
852
|
+
phone: user.phone ?? null,
|
|
853
|
+
profilePictureId: user.profilePictureId ?? null
|
|
854
|
+
};
|
|
855
|
+
if (requiresEmailVerification) {
|
|
856
|
+
return {
|
|
857
|
+
user: userInfo,
|
|
858
|
+
companyFeatureEnabled,
|
|
859
|
+
requiresEmailVerification: true
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
const loginResponse = this.generateLoginResponse(user, companyFeatureEnabled);
|
|
863
|
+
return {
|
|
864
|
+
...loginResponse,
|
|
865
|
+
requiresEmailVerification: false
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/** Generate registration response with company - tokens included only if email verification not required */ generateRegistrationResponseWithCompany(user, company, branch, requiresEmailVerification = false) {
|
|
869
|
+
const userInfo = {
|
|
870
|
+
id: user.id,
|
|
871
|
+
email: user.email,
|
|
872
|
+
name: user.name ?? undefined,
|
|
873
|
+
phone: user.phone ?? null,
|
|
874
|
+
profilePictureId: user.profilePictureId ?? null
|
|
875
|
+
};
|
|
876
|
+
const companyInfo = {
|
|
877
|
+
id: company.id,
|
|
878
|
+
name: company.name,
|
|
879
|
+
slug: company.slug,
|
|
880
|
+
logoId: company.logoId ?? null
|
|
881
|
+
};
|
|
882
|
+
const branchInfo = branch ? {
|
|
883
|
+
id: branch.id,
|
|
884
|
+
name: branch.name,
|
|
885
|
+
slug: branch.slug,
|
|
886
|
+
logoId: branch.logoId ?? null
|
|
887
|
+
} : undefined;
|
|
888
|
+
if (requiresEmailVerification) {
|
|
889
|
+
return {
|
|
890
|
+
user: userInfo,
|
|
891
|
+
company: companyInfo,
|
|
892
|
+
branch: branchInfo,
|
|
893
|
+
companyFeatureEnabled: true,
|
|
894
|
+
requiresEmailVerification: true
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
const loginResponse = this.generateLoginResponseWithCompany(user, company, branch);
|
|
898
|
+
return {
|
|
899
|
+
...loginResponse,
|
|
900
|
+
requiresEmailVerification: false
|
|
901
|
+
};
|
|
902
|
+
}
|
|
789
903
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
790
|
-
constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, injectedUserRepository, injectedPermissionRepository, injectedCompanyRepository, injectedBranchRepository){
|
|
904
|
+
constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService, injectedUserRepository, injectedPermissionRepository, injectedCompanyRepository, injectedBranchRepository){
|
|
791
905
|
_define_property(this, "jwtService", void 0);
|
|
792
906
|
_define_property(this, "authConfig", void 0);
|
|
793
907
|
_define_property(this, "cacheManager", void 0);
|
|
794
908
|
_define_property(this, "dataSourceProvider", void 0);
|
|
795
909
|
_define_property(this, "sessionService", void 0);
|
|
910
|
+
_define_property(this, "authEmailService", void 0);
|
|
796
911
|
_define_property(this, "injectedUserRepository", void 0);
|
|
797
912
|
_define_property(this, "injectedPermissionRepository", void 0);
|
|
798
913
|
_define_property(this, "injectedCompanyRepository", void 0);
|
|
@@ -803,6 +918,7 @@ export class AuthenticationService {
|
|
|
803
918
|
this.cacheManager = cacheManager;
|
|
804
919
|
this.dataSourceProvider = dataSourceProvider;
|
|
805
920
|
this.sessionService = sessionService;
|
|
921
|
+
this.authEmailService = authEmailService;
|
|
806
922
|
this.injectedUserRepository = injectedUserRepository;
|
|
807
923
|
this.injectedPermissionRepository = injectedPermissionRepository;
|
|
808
924
|
this.injectedCompanyRepository = injectedCompanyRepository;
|
|
@@ -823,13 +939,15 @@ AuthenticationService = _ts_decorate([
|
|
|
823
939
|
_ts_param(4, Optional()),
|
|
824
940
|
_ts_param(4, Inject(CompanySelectionSessionService)),
|
|
825
941
|
_ts_param(5, Optional()),
|
|
826
|
-
_ts_param(5,
|
|
942
|
+
_ts_param(5, Inject(AuthEmailService)),
|
|
827
943
|
_ts_param(6, Optional()),
|
|
828
|
-
_ts_param(6, InjectRepository(
|
|
944
|
+
_ts_param(6, InjectRepository(User)),
|
|
829
945
|
_ts_param(7, Optional()),
|
|
830
|
-
_ts_param(7, InjectRepository(
|
|
946
|
+
_ts_param(7, InjectRepository(UserCompanyPermission)),
|
|
831
947
|
_ts_param(8, Optional()),
|
|
832
|
-
_ts_param(8, InjectRepository(
|
|
948
|
+
_ts_param(8, InjectRepository(Company)),
|
|
949
|
+
_ts_param(9, Optional()),
|
|
950
|
+
_ts_param(9, InjectRepository(CompanyBranch)),
|
|
833
951
|
_ts_metadata("design:type", Function),
|
|
834
952
|
_ts_metadata("design:paramtypes", [
|
|
835
953
|
typeof JwtService === "undefined" ? Object : JwtService,
|
|
@@ -837,6 +955,7 @@ AuthenticationService = _ts_decorate([
|
|
|
837
955
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
838
956
|
typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
|
|
839
957
|
typeof CompanySelectionSessionService === "undefined" ? Object : CompanySelectionSessionService,
|
|
958
|
+
typeof AuthEmailService === "undefined" ? Object : AuthEmailService,
|
|
840
959
|
typeof Repository === "undefined" ? Object : Repository,
|
|
841
960
|
typeof Repository === "undefined" ? Object : Repository,
|
|
842
961
|
typeof Repository === "undefined" ? Object : Repository,
|
package/fesm/services/index.js
CHANGED
|
@@ -28,12 +28,13 @@ function _ts_param(paramIndex, decorator) {
|
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
30
|
import { ErrorHandler } from '@flusys/nestjs-shared/utils';
|
|
31
|
-
import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
31
|
+
import { BadRequestException, Inject, Injectable, NotFoundException, Optional, Scope } from '@nestjs/common';
|
|
32
32
|
import * as bcrypt from 'bcrypt';
|
|
33
33
|
import { AuthConfigService } from './auth-config.service';
|
|
34
34
|
import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
35
|
-
import { BCRYPT_SALT_ROUNDS } from '../config';
|
|
35
|
+
import { BCRYPT_SALT_ROUNDS, USER_ENRICHER } from '../config';
|
|
36
36
|
import { PermissionType } from '../enums';
|
|
37
|
+
import { IUserEnricher } from '../interfaces/user-enricher.interface';
|
|
37
38
|
import { UserCompanyPermission } from '../entities/user-company-permission.entity';
|
|
38
39
|
import { User } from '../entities/user.entity';
|
|
39
40
|
export class UserService extends RequestScopedApiService {
|
|
@@ -119,8 +120,8 @@ export class UserService extends RequestScopedApiService {
|
|
|
119
120
|
/** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
|
|
120
121
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
121
122
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
122
|
-
// Hash password if provided
|
|
123
|
-
if (dto.password) {
|
|
123
|
+
// Hash password if provided (CreateUserDto has password field)
|
|
124
|
+
if ('password' in dto && dto.password) {
|
|
124
125
|
entity.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
125
126
|
}
|
|
126
127
|
return entity;
|
|
@@ -162,7 +163,11 @@ export class UserService extends RequestScopedApiService {
|
|
|
162
163
|
}
|
|
163
164
|
/** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
|
|
164
165
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
165
|
-
|
|
166
|
+
let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
|
|
167
|
+
// Apply user enricher query modifications if available
|
|
168
|
+
if (this.userEnricher?.enrichListQuery) {
|
|
169
|
+
await this.userEnricher.enrichListQuery(query, user);
|
|
170
|
+
}
|
|
166
171
|
// Only apply company filtering if:
|
|
167
172
|
// 1. Company feature is enabled at bootstrap
|
|
168
173
|
// 2. User has company context
|
|
@@ -282,16 +287,140 @@ export class UserService extends RequestScopedApiService {
|
|
|
282
287
|
await queryRunner.release();
|
|
283
288
|
}
|
|
284
289
|
}
|
|
285
|
-
/**
|
|
286
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Get user profile with extras from enricher
|
|
292
|
+
* Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
|
|
293
|
+
*/ async getProfileWithExtras(userId, user) {
|
|
294
|
+
await this.ensureRepositoryInitialized();
|
|
295
|
+
const userEntity = await this.repository.findOne({
|
|
296
|
+
where: {
|
|
297
|
+
id: userId
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
if (!userEntity) {
|
|
301
|
+
throw new NotFoundException('User not found');
|
|
302
|
+
}
|
|
303
|
+
const userData = this.convertEntityToResponseDto(userEntity, false);
|
|
304
|
+
// Get extras from enricher if available
|
|
305
|
+
let extras;
|
|
306
|
+
if (this.userEnricher?.getProfileExtras) {
|
|
307
|
+
extras = await this.userEnricher.getProfileExtras(userId, user);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
user: userData,
|
|
311
|
+
extras
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get enriched user list
|
|
316
|
+
* Applies enricher transformations after query execution
|
|
317
|
+
*/ async getEnrichedList(users, user) {
|
|
318
|
+
if (this.userEnricher?.enrichListItems) {
|
|
319
|
+
return this.userEnricher.enrichListItems(users, user);
|
|
320
|
+
}
|
|
321
|
+
return users;
|
|
322
|
+
}
|
|
323
|
+
// ============================================
|
|
324
|
+
// Multi-Step Profile Methods (via Enricher)
|
|
325
|
+
// ============================================
|
|
326
|
+
/**
|
|
327
|
+
* Get profile section definitions from enricher
|
|
328
|
+
*/ async getProfileSections() {
|
|
329
|
+
if (this.userEnricher?.getProfileSections) {
|
|
330
|
+
return this.userEnricher.getProfileSections();
|
|
331
|
+
}
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get specific profile section data
|
|
336
|
+
*/ async getProfileSectionData(userId, sectionId, user) {
|
|
337
|
+
await this.ensureRepositoryInitialized();
|
|
338
|
+
// Verify user exists
|
|
339
|
+
const userEntity = await this.repository.findOne({
|
|
340
|
+
where: {
|
|
341
|
+
id: userId
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
if (!userEntity) {
|
|
345
|
+
throw new NotFoundException('User not found');
|
|
346
|
+
}
|
|
347
|
+
if (this.userEnricher?.getProfileSectionData) {
|
|
348
|
+
return this.userEnricher.getProfileSectionData(userId, sectionId, user);
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Update specific profile section
|
|
354
|
+
*/ async updateProfileSection(userId, sectionId, data, user) {
|
|
355
|
+
await this.ensureRepositoryInitialized();
|
|
356
|
+
if (!this.dataSource) {
|
|
357
|
+
throw new BadRequestException('DataSource not available');
|
|
358
|
+
}
|
|
359
|
+
// Verify user exists
|
|
360
|
+
const userEntity = await this.repository.findOne({
|
|
361
|
+
where: {
|
|
362
|
+
id: userId
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
if (!userEntity) {
|
|
366
|
+
throw new NotFoundException('User not found');
|
|
367
|
+
}
|
|
368
|
+
if (!this.userEnricher?.updateProfileSection) {
|
|
369
|
+
throw new BadRequestException('Profile sections not supported');
|
|
370
|
+
}
|
|
371
|
+
const queryRunner = this.dataSource.createQueryRunner();
|
|
372
|
+
await queryRunner.connect();
|
|
373
|
+
await queryRunner.startTransaction();
|
|
374
|
+
try {
|
|
375
|
+
await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
|
|
376
|
+
await queryRunner.commitTransaction();
|
|
377
|
+
await this.clearCacheForId([
|
|
378
|
+
userEntity
|
|
379
|
+
]);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
await queryRunner.rollbackTransaction();
|
|
382
|
+
ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
|
|
383
|
+
entity: this.entityName,
|
|
384
|
+
sectionId
|
|
385
|
+
});
|
|
386
|
+
ErrorHandler.rethrowError(error);
|
|
387
|
+
} finally{
|
|
388
|
+
await queryRunner.release();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Get profile completion percentage
|
|
393
|
+
*/ async getProfileCompletion(userId, user) {
|
|
394
|
+
await this.ensureRepositoryInitialized();
|
|
395
|
+
// Verify user exists
|
|
396
|
+
const userEntity = await this.repository.findOne({
|
|
397
|
+
where: {
|
|
398
|
+
id: userId
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
if (!userEntity) {
|
|
402
|
+
throw new NotFoundException('User not found');
|
|
403
|
+
}
|
|
404
|
+
if (this.userEnricher?.calculateProfileCompletion) {
|
|
405
|
+
return this.userEnricher.calculateProfileCompletion(userId, user);
|
|
406
|
+
}
|
|
407
|
+
return 100; // Default to 100% if no enricher
|
|
408
|
+
}
|
|
409
|
+
/** Update user profile with password handling and extras */ async profile(dto, user) {
|
|
410
|
+
// Ensure repository and dataSource are initialized
|
|
411
|
+
await this.ensureRepositoryInitialized();
|
|
287
412
|
if (!this.dataSource) {
|
|
288
413
|
throw new BadRequestException('DataSource not available');
|
|
289
414
|
}
|
|
415
|
+
const userId = dto.id ?? user.id;
|
|
416
|
+
// Validate extras if enricher provides validation
|
|
417
|
+
if (dto.extras && this.userEnricher?.validateProfileExtras) {
|
|
418
|
+
await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
|
|
419
|
+
}
|
|
290
420
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
291
421
|
await queryRunner.connect();
|
|
292
422
|
await queryRunner.startTransaction();
|
|
293
423
|
try {
|
|
294
|
-
const userId = dto.id ?? user.id;
|
|
295
424
|
const findUser = await queryRunner.manager.findOne(this.repository.target, {
|
|
296
425
|
where: {
|
|
297
426
|
id: userId
|
|
@@ -310,10 +439,14 @@ export class UserService extends RequestScopedApiService {
|
|
|
310
439
|
const salt = await bcrypt.genSalt();
|
|
311
440
|
findUser.password = await bcrypt.hash(dto.password, salt);
|
|
312
441
|
}
|
|
313
|
-
// Merge other fields (excluding password)
|
|
314
|
-
const { password, ...restDto } = dto;
|
|
442
|
+
// Merge other fields (excluding password and extras)
|
|
443
|
+
const { password, extras, ...restDto } = dto;
|
|
315
444
|
Object.assign(findUser, restDto);
|
|
316
445
|
await queryRunner.manager.save(this.repository.target, findUser);
|
|
446
|
+
// Handle extras update via enricher
|
|
447
|
+
if (extras && this.userEnricher?.updateProfileExtras) {
|
|
448
|
+
await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
|
|
449
|
+
}
|
|
317
450
|
await queryRunner.commitTransaction();
|
|
318
451
|
await this.clearCacheForId([
|
|
319
452
|
findUser
|
|
@@ -329,8 +462,8 @@ export class UserService extends RequestScopedApiService {
|
|
|
329
462
|
await queryRunner.release();
|
|
330
463
|
}
|
|
331
464
|
}
|
|
332
|
-
constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
|
|
333
|
-
super('user', null, cacheManager, utilsService, UserService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "authConfig", void 0), _define_property(this, "permissionRepository", void 0), _define_property(this, "dataSource", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig;
|
|
465
|
+
constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
|
|
466
|
+
super('user', null, cacheManager, utilsService, UserService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "authConfig", void 0), _define_property(this, "userEnricher", void 0), _define_property(this, "permissionRepository", void 0), _define_property(this, "dataSource", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig, this.userEnricher = userEnricher;
|
|
334
467
|
}
|
|
335
468
|
}
|
|
336
469
|
UserService = _ts_decorate([
|
|
@@ -341,11 +474,14 @@ UserService = _ts_decorate([
|
|
|
341
474
|
_ts_param(1, Inject(UtilsService)),
|
|
342
475
|
_ts_param(2, Inject(AuthDataSourceProvider)),
|
|
343
476
|
_ts_param(3, Inject(AuthConfigService)),
|
|
477
|
+
_ts_param(4, Optional()),
|
|
478
|
+
_ts_param(4, Inject(USER_ENRICHER)),
|
|
344
479
|
_ts_metadata("design:type", Function),
|
|
345
480
|
_ts_metadata("design:paramtypes", [
|
|
346
481
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
347
482
|
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
348
483
|
typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
|
|
349
|
-
typeof AuthConfigService === "undefined" ? Object : AuthConfigService
|
|
484
|
+
typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
|
|
485
|
+
typeof IUserEnricher === "undefined" ? Object : IUserEnricher
|
|
350
486
|
])
|
|
351
487
|
], UserService);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface IAuthEmailProvider {
|
|
2
|
+
sendPasswordResetEmail(email: string, token: string, resetUrl: string): Promise<void>;
|
|
3
|
+
sendVerificationEmail(email: string, token: string, verifyUrl: string): Promise<void>;
|
|
4
|
+
sendWelcomeEmail?(email: string, name: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare const AUTH_EMAIL_PROVIDER = "AUTH_EMAIL_PROVIDER";
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
|
|
2
|
-
import { ModuleMetadata, Type } from '@nestjs/common';
|
|
2
|
+
import { ModuleMetadata, Provider, Type } from '@nestjs/common';
|
|
3
3
|
export interface IAuthModuleConfig extends IDataSourceServiceOptions {
|
|
4
4
|
jwtSecret?: string;
|
|
5
5
|
jwtExpiration?: string;
|
|
6
6
|
refreshTokenSecret?: string;
|
|
7
7
|
refreshTokenExpiration?: string;
|
|
8
8
|
refreshTokenCookieName?: string;
|
|
9
|
+
frontendUrl?: string;
|
|
9
10
|
}
|
|
10
11
|
export interface AuthModuleOptions extends IDynamicModuleConfig {
|
|
11
12
|
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
12
13
|
config?: IAuthModuleConfig;
|
|
14
|
+
providers?: Provider[];
|
|
13
15
|
}
|
|
14
16
|
export interface AuthOptionsFactory extends IModuleOptionsFactory<IAuthModuleConfig> {
|
|
15
17
|
createAuthOptions(): Promise<IAuthModuleConfig> | IAuthModuleConfig;
|
|
@@ -21,4 +23,5 @@ export interface AuthModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>,
|
|
|
21
23
|
inject?: any[];
|
|
22
24
|
useClass?: Type<AuthOptionsFactory>;
|
|
23
25
|
useExisting?: Type<AuthOptionsFactory>;
|
|
26
|
+
providers?: Provider[];
|
|
24
27
|
}
|
|
@@ -51,3 +51,17 @@ export interface LoginResponseRequiresSelection {
|
|
|
51
51
|
user: IUserMini;
|
|
52
52
|
companies: CompanyWithBranches[];
|
|
53
53
|
}
|
|
54
|
+
export interface RegistrationResponse {
|
|
55
|
+
accessToken?: string;
|
|
56
|
+
refreshToken?: string;
|
|
57
|
+
user: IUserMini;
|
|
58
|
+
company?: ICompanyMini;
|
|
59
|
+
branch?: ICompanyBranchMini;
|
|
60
|
+
companyFeatureEnabled?: boolean;
|
|
61
|
+
requiresEmailVerification?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface LoginResponseRequiresEmailVerification {
|
|
64
|
+
requiresEmailVerification: true;
|
|
65
|
+
email: string;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
package/interfaces/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './authentication.interface';
|
|
2
|
+
export * from './auth-email-provider.interface';
|
|
2
3
|
export * from './company-branch.interface';
|
|
3
4
|
export * from './company.interface';
|
|
4
5
|
export * from './user.interface';
|
|
6
|
+
export * from './user-enricher.interface';
|
|
5
7
|
export * from './auth-module-options.interface';
|