@nauth-toolkit/core 0.1.32 → 0.1.34
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/dist/dto/admin-signup-social.dto.d.ts +257 -0
- package/dist/dto/admin-signup-social.dto.d.ts.map +1 -0
- package/dist/dto/admin-signup-social.dto.js +389 -0
- package/dist/dto/admin-signup-social.dto.js.map +1 -0
- package/dist/dto/delete-user.dto.d.ts +71 -0
- package/dist/dto/delete-user.dto.d.ts.map +1 -0
- package/dist/dto/delete-user.dto.js +82 -0
- package/dist/dto/delete-user.dto.js.map +1 -0
- package/dist/dto/disable-user.dto.d.ts +61 -0
- package/dist/dto/disable-user.dto.d.ts.map +1 -0
- package/dist/dto/disable-user.dto.js +86 -0
- package/dist/dto/disable-user.dto.js.map +1 -0
- package/dist/dto/enable-user.dto.d.ts +44 -0
- package/dist/dto/enable-user.dto.d.ts.map +1 -0
- package/dist/dto/enable-user.dto.js +63 -0
- package/dist/dto/enable-user.dto.js.map +1 -0
- package/dist/dto/get-users.dto.d.ts +154 -0
- package/dist/dto/get-users.dto.d.ts.map +1 -0
- package/dist/dto/get-users.dto.js +250 -0
- package/dist/dto/get-users.dto.js.map +1 -0
- package/dist/dto/index.d.ts +5 -0
- package/dist/dto/index.d.ts.map +1 -1
- package/dist/dto/index.js +5 -0
- package/dist/dto/index.js.map +1 -1
- package/dist/dto/user-response.dto.d.ts +5 -0
- package/dist/dto/user-response.dto.d.ts.map +1 -1
- package/dist/dto/user-response.dto.js +6 -0
- package/dist/dto/user-response.dto.js.map +1 -1
- package/dist/dto/verify-email.dto.d.ts +10 -0
- package/dist/dto/verify-email.dto.d.ts.map +1 -1
- package/dist/dto/verify-email.dto.js +16 -0
- package/dist/dto/verify-email.dto.js.map +1 -1
- package/dist/entities/user.entity.d.ts +18 -2
- package/dist/entities/user.entity.d.ts.map +1 -1
- package/dist/entities/user.entity.js +18 -2
- package/dist/entities/user.entity.js.map +1 -1
- package/dist/enums/auth-audit-event-type.enum.d.ts +5 -0
- package/dist/enums/auth-audit-event-type.enum.d.ts.map +1 -1
- package/dist/enums/auth-audit-event-type.enum.js +5 -0
- package/dist/enums/auth-audit-event-type.enum.js.map +1 -1
- package/dist/enums/error-codes.enum.d.ts +13 -0
- package/dist/enums/error-codes.enum.d.ts.map +1 -1
- package/dist/enums/error-codes.enum.js +13 -0
- package/dist/enums/error-codes.enum.js.map +1 -1
- package/dist/services/auth.service.d.ts +172 -2
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +867 -2
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/email-verification.service.d.ts.map +1 -1
- package/dist/services/email-verification.service.js +7 -7
- package/dist/services/email-verification.service.js.map +1 -1
- package/dist/services/social-auth-base.service.d.ts +5 -1
- package/dist/services/social-auth-base.service.d.ts.map +1 -1
- package/dist/services/social-auth-base.service.js +62 -2
- package/dist/services/social-auth-base.service.js.map +1 -1
- package/dist/services/social-auth.service.d.ts +2 -1
- package/dist/services/social-auth.service.d.ts.map +1 -1
- package/dist/services/social-auth.service.js +5 -1
- package/dist/services/social-auth.service.js.map +1 -1
- package/dist/utils/setup/init-services.d.ts.map +1 -1
- package/dist/utils/setup/init-services.js +2 -1
- package/dist/utils/setup/init-services.js.map +1 -1
- package/package.json +1 -1
|
@@ -39,6 +39,11 @@ const risk_factor_enum_1 = require("../enums/risk-factor.enum");
|
|
|
39
39
|
const context_storage_1 = require("../utils/context-storage");
|
|
40
40
|
const signup_dto_1 = require("../dto/signup.dto");
|
|
41
41
|
const admin_signup_dto_1 = require("../dto/admin-signup.dto");
|
|
42
|
+
const admin_signup_social_dto_1 = require("../dto/admin-signup-social.dto");
|
|
43
|
+
const delete_user_dto_1 = require("../dto/delete-user.dto");
|
|
44
|
+
const get_users_dto_1 = require("../dto/get-users.dto");
|
|
45
|
+
const disable_user_dto_1 = require("../dto/disable-user.dto");
|
|
46
|
+
const enable_user_dto_1 = require("../dto/enable-user.dto");
|
|
42
47
|
const login_dto_1 = require("../dto/login.dto");
|
|
43
48
|
const change_password_request_dto_1 = require("../dto/change-password-request.dto");
|
|
44
49
|
const update_user_attributes_request_dto_1 = require("../dto/update-user-attributes-request.dto");
|
|
@@ -93,12 +98,26 @@ class AuthService {
|
|
|
93
98
|
mfaDeviceRepository;
|
|
94
99
|
trustedDeviceService;
|
|
95
100
|
passwordResetService;
|
|
101
|
+
socialAuthService;
|
|
102
|
+
sessionRepository;
|
|
103
|
+
verificationTokenRepository;
|
|
104
|
+
socialAccountRepository;
|
|
105
|
+
challengeSessionRepository;
|
|
106
|
+
authAuditRepository;
|
|
107
|
+
trustedDeviceRepository;
|
|
96
108
|
constructor(userRepository, loginAttemptRepository, passwordService, jwtService, sessionService, challengeService, challengeHelper, emailVerificationService, clientInfoService, accountLockoutStorage, config, logger, auditService, // Optional - audit trail service (enabled via config.auditLogs.enabled)
|
|
97
109
|
phoneVerificationService, // Optional - only available when SMS provider is configured
|
|
98
110
|
mfaService, // Optional - available when MFA modules are imported
|
|
99
111
|
mfaDeviceRepository, // Optional - available when MFA modules are imported
|
|
100
112
|
trustedDeviceService, // Optional - only available when rememberDevices is not 'never'
|
|
101
|
-
passwordResetService
|
|
113
|
+
passwordResetService, // Optional - only available when configured by framework adapter
|
|
114
|
+
socialAuthService, // Optional - only available when social auth is configured
|
|
115
|
+
sessionRepository, // Optional - for cascade deletion
|
|
116
|
+
verificationTokenRepository, // Optional - for cascade deletion
|
|
117
|
+
socialAccountRepository, // Optional - for cascade deletion
|
|
118
|
+
challengeSessionRepository, // Optional - for cascade deletion
|
|
119
|
+
authAuditRepository, // Optional - for cascade deletion
|
|
120
|
+
trustedDeviceRepository) {
|
|
102
121
|
this.userRepository = userRepository;
|
|
103
122
|
this.loginAttemptRepository = loginAttemptRepository;
|
|
104
123
|
this.passwordService = passwordService;
|
|
@@ -117,6 +136,13 @@ class AuthService {
|
|
|
117
136
|
this.mfaDeviceRepository = mfaDeviceRepository;
|
|
118
137
|
this.trustedDeviceService = trustedDeviceService;
|
|
119
138
|
this.passwordResetService = passwordResetService;
|
|
139
|
+
this.socialAuthService = socialAuthService;
|
|
140
|
+
this.sessionRepository = sessionRepository;
|
|
141
|
+
this.verificationTokenRepository = verificationTokenRepository;
|
|
142
|
+
this.socialAccountRepository = socialAccountRepository;
|
|
143
|
+
this.challengeSessionRepository = challengeSessionRepository;
|
|
144
|
+
this.authAuditRepository = authAuditRepository;
|
|
145
|
+
this.trustedDeviceRepository = trustedDeviceRepository;
|
|
120
146
|
this.logger?.log?.('AuthService initialized');
|
|
121
147
|
}
|
|
122
148
|
// ============================================================================
|
|
@@ -504,6 +530,753 @@ class AuthService {
|
|
|
504
530
|
};
|
|
505
531
|
}
|
|
506
532
|
// ============================================================================
|
|
533
|
+
// Admin Social Signup
|
|
534
|
+
// ============================================================================
|
|
535
|
+
/**
|
|
536
|
+
* Administrative social user import with override capabilities
|
|
537
|
+
*
|
|
538
|
+
* Allows administrators to import existing social users from external platforms
|
|
539
|
+
* (e.g., Cognito, Auth0) into nauth with:
|
|
540
|
+
* - Bypass email/phone verification requirements
|
|
541
|
+
* - Optional password for hybrid social+password accounts
|
|
542
|
+
* - Social account linkage (provider + providerId)
|
|
543
|
+
* - Automatic user flag updates (hasSocialAuth)
|
|
544
|
+
*
|
|
545
|
+
* Use case: Migrating users from external authentication platforms while
|
|
546
|
+
* preserving their social login connections for transparent future logins.
|
|
547
|
+
*
|
|
548
|
+
* Security:
|
|
549
|
+
* - No built-in authentication - endpoint must be protected by framework adapter
|
|
550
|
+
* - All duplicate checks enforced (email, username, phone, provider+providerId)
|
|
551
|
+
* - Password policy enforced if password provided
|
|
552
|
+
* - Audit trail records admin-imported social accounts
|
|
553
|
+
*
|
|
554
|
+
* @param dto - Admin social signup DTO with social account details
|
|
555
|
+
* @returns User object and social account confirmation
|
|
556
|
+
* @throws {NAuthException} EMAIL_EXISTS | USERNAME_EXISTS | PHONE_EXISTS | SOCIAL_ACCOUNT_EXISTS | WEAK_PASSWORD
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```typescript
|
|
560
|
+
* // Import social-only user from Cognito
|
|
561
|
+
* // Note: Email is automatically verified for social imports (like normal social signup)
|
|
562
|
+
* const result = await authService.adminSignupSocial({
|
|
563
|
+
* email: 'user@example.com',
|
|
564
|
+
* provider: 'google',
|
|
565
|
+
* providerId: 'google_12345',
|
|
566
|
+
* providerEmail: 'user@gmail.com',
|
|
567
|
+
* socialMetadata: { sub: 'google_12345', given_name: 'John' },
|
|
568
|
+
* });
|
|
569
|
+
*
|
|
570
|
+
* // Import hybrid user with password + social
|
|
571
|
+
* const result = await authService.adminSignupSocial({
|
|
572
|
+
* email: 'user@example.com',
|
|
573
|
+
* password: 'SecurePass123!',
|
|
574
|
+
* provider: 'apple',
|
|
575
|
+
* providerId: 'apple_67890',
|
|
576
|
+
* });
|
|
577
|
+
* ```
|
|
578
|
+
*/
|
|
579
|
+
async adminSignupSocial(dto) {
|
|
580
|
+
// Ensure DTO is validated (supports direct usage without framework validation)
|
|
581
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(admin_signup_social_dto_1.AdminSignupSocialDTO, dto);
|
|
582
|
+
// Get client info from request context (transparent!)
|
|
583
|
+
const clientInfo = this.clientInfoService.get();
|
|
584
|
+
this.logger?.log?.(`Admin social signup attempt for email: ${dto.email}, provider: ${dto.provider}`);
|
|
585
|
+
this.logger?.debug?.(`Admin social signup details: { email: ${dto.email}, username: ${dto.username || 'none'}, provider: ${dto.provider}, providerId: ${dto.providerId}, ip: ${clientInfo.ipAddress} }`);
|
|
586
|
+
// Skip signup.enabled check (admin bypass)
|
|
587
|
+
// Check if user already exists (email and username)
|
|
588
|
+
this.logger?.debug?.(`Checking if user exists: ${dto.email}`);
|
|
589
|
+
const existingUserByEmail = await this.userRepository.findOne({
|
|
590
|
+
where: { email: dto.email },
|
|
591
|
+
});
|
|
592
|
+
if (existingUserByEmail) {
|
|
593
|
+
this.logger?.warn?.(`Admin social signup failed - user already exists: ${dto.email}`);
|
|
594
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
|
|
595
|
+
}
|
|
596
|
+
// Check for duplicate username if provided
|
|
597
|
+
if (dto.username) {
|
|
598
|
+
this.logger?.debug?.(`Checking if username exists: ${dto.username}`);
|
|
599
|
+
const existingUserByUsername = await this.userRepository.findOne({
|
|
600
|
+
where: { username: dto.username },
|
|
601
|
+
});
|
|
602
|
+
if (existingUserByUsername) {
|
|
603
|
+
this.logger?.warn?.(`Admin social signup failed - username already exists: ${dto.username}`);
|
|
604
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Check for duplicate phone if provided and duplicates not allowed
|
|
608
|
+
if (dto.phone && !this.config.signup?.allowDuplicatePhones) {
|
|
609
|
+
this.logger?.debug?.(`Checking if phone exists: ${dto.phone}`);
|
|
610
|
+
const existingUserByPhone = await this.userRepository.findOne({
|
|
611
|
+
where: { phone: dto.phone },
|
|
612
|
+
});
|
|
613
|
+
if (existingUserByPhone) {
|
|
614
|
+
this.logger?.warn?.(`Admin social signup failed - phone already exists: ${dto.phone}`);
|
|
615
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Check for duplicate provider+providerId
|
|
619
|
+
if (!this.socialAuthService) {
|
|
620
|
+
this.logger?.error?.('SocialAuthService not available - cannot import social user');
|
|
621
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SOCIAL_CONFIG_MISSING, 'Social authentication is not configured');
|
|
622
|
+
}
|
|
623
|
+
this.logger?.debug?.(`Checking if social account exists: ${dto.provider}:${dto.providerId}`);
|
|
624
|
+
const existingSocialAccount = await this.socialAuthService.findSocialAccountByProvider(dto.provider, dto.providerId);
|
|
625
|
+
if (existingSocialAccount) {
|
|
626
|
+
this.logger?.warn?.(`Admin social signup failed - social account already exists: ${dto.provider}:${dto.providerId}`);
|
|
627
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SOCIAL_ACCOUNT_EXISTS, 'This social account is already registered');
|
|
628
|
+
}
|
|
629
|
+
// Handle password (optional for hybrid accounts)
|
|
630
|
+
let passwordHash;
|
|
631
|
+
if (dto.password) {
|
|
632
|
+
// Validate password policy
|
|
633
|
+
this.logger?.debug?.('Validating password against policy');
|
|
634
|
+
const passwordValidation = await this.passwordService.validatePassword(dto.password, {
|
|
635
|
+
email: dto.email,
|
|
636
|
+
username: dto.username,
|
|
637
|
+
});
|
|
638
|
+
if (!passwordValidation.valid) {
|
|
639
|
+
this.logger?.warn?.(`Password validation failed for ${dto.email}: ${passwordValidation.errors.join(', ')}`);
|
|
640
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.WEAK_PASSWORD, passwordValidation.errors.join(', '), {
|
|
641
|
+
errors: passwordValidation.errors,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
// Hash password
|
|
645
|
+
passwordHash = await this.passwordService.hashPassword(dto.password);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
// Social-only user: no password (NULL in database)
|
|
649
|
+
passwordHash = null;
|
|
650
|
+
}
|
|
651
|
+
// Create user with override flags
|
|
652
|
+
// Note: Email is always verified for social imports (like normal social signup)
|
|
653
|
+
this.logger?.debug?.(`Creating admin social user record for: ${dto.email} || ${dto.username} || ${dto.phone} (isEmailVerified: true [auto-verified for social], isPhoneVerified: ${dto.isPhoneVerified || false})`);
|
|
654
|
+
const user = this.userRepository.create({
|
|
655
|
+
email: dto.email,
|
|
656
|
+
username: dto.username,
|
|
657
|
+
firstName: dto.firstName,
|
|
658
|
+
lastName: dto.lastName,
|
|
659
|
+
phone: dto.phone,
|
|
660
|
+
passwordHash, // null for social-only, hashed string for hybrid accounts
|
|
661
|
+
passwordChangedAt: dto.password ? new Date() : null, // Only set if password provided
|
|
662
|
+
isEmailVerified: true, // Always verified for social imports (like normal social signup)
|
|
663
|
+
isPhoneVerified: dto.isPhoneVerified ?? false, // Use DTO value or default to false
|
|
664
|
+
mustChangePassword: dto.mustChangePassword ?? false, // Use DTO value or default to false
|
|
665
|
+
isActive: true, // Always active
|
|
666
|
+
metadata: dto.metadata,
|
|
667
|
+
hasSocialAuth: true, // Set immediately since we know this is a social user
|
|
668
|
+
socialProviders: [dto.provider], // Set immediately with the provider from DTO
|
|
669
|
+
});
|
|
670
|
+
let savedUser;
|
|
671
|
+
try {
|
|
672
|
+
savedUser = (await this.userRepository.save(user));
|
|
673
|
+
this.logger?.log?.(`Admin social user created successfully: ${dto.email} (sub: ${savedUser.sub}, provider: ${dto.provider})`);
|
|
674
|
+
// Create social account linkage
|
|
675
|
+
this.logger?.debug?.(`Creating social account linkage: ${dto.provider}:${dto.providerId}`);
|
|
676
|
+
await this.socialAuthService.createOrUpdateSocialAccount(savedUser.id, dto.provider, dto.providerId, dto.providerEmail || null, dto.socialMetadata);
|
|
677
|
+
this.logger?.log?.(`Social account linked successfully: ${dto.provider}:${dto.providerId}`);
|
|
678
|
+
// Update savedUser in memory to reflect the updated social flags (no additional query needed)
|
|
679
|
+
// updateUserSocialFlags() has already updated the DB, we just sync the in-memory object
|
|
680
|
+
savedUser.hasSocialAuth = true;
|
|
681
|
+
savedUser.socialProviders = [dto.provider];
|
|
682
|
+
// ============================================================================
|
|
683
|
+
// Audit: Record account creation by admin (social import)
|
|
684
|
+
// ============================================================================
|
|
685
|
+
try {
|
|
686
|
+
await this.auditService?.recordEvent({
|
|
687
|
+
userId: savedUser.id,
|
|
688
|
+
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_CREATED,
|
|
689
|
+
eventStatus: 'INFO',
|
|
690
|
+
authMethod: 'admin-social',
|
|
691
|
+
// Client info automatically included from context
|
|
692
|
+
metadata: {
|
|
693
|
+
email: savedUser.email,
|
|
694
|
+
username: savedUser.username || null,
|
|
695
|
+
createdByAdmin: true,
|
|
696
|
+
adminIdentifier: clientInfo.ipAddress || 'unknown',
|
|
697
|
+
isEmailVerified: savedUser.isEmailVerified,
|
|
698
|
+
isPhoneVerified: savedUser.isPhoneVerified,
|
|
699
|
+
mustChangePassword: savedUser.mustChangePassword,
|
|
700
|
+
provider: dto.provider,
|
|
701
|
+
providerId: dto.providerId,
|
|
702
|
+
hasPassword: !!dto.password,
|
|
703
|
+
socialImport: true,
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
catch (auditError) {
|
|
708
|
+
// Non-blocking: Log but continue
|
|
709
|
+
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
710
|
+
this.logger?.error?.(`Failed to record ACCOUNT_CREATED audit event: ${errorMessage}`, {
|
|
711
|
+
error: auditError,
|
|
712
|
+
userId: savedUser.id,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
// Handle database constraint violations gracefully
|
|
718
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === '23505') {
|
|
719
|
+
// PostgreSQL unique constraint violation
|
|
720
|
+
const dbError = error;
|
|
721
|
+
if (dbError.detail?.includes('email')) {
|
|
722
|
+
this.logger?.warn?.(`Admin social signup failed - email constraint violation: ${dto.email}`);
|
|
723
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
|
|
724
|
+
}
|
|
725
|
+
else if (dbError.detail?.includes('username')) {
|
|
726
|
+
this.logger?.warn?.(`Admin social signup failed - username constraint violation: ${dto.username}`);
|
|
727
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
|
|
728
|
+
}
|
|
729
|
+
else if (dbError.detail?.includes('phone')) {
|
|
730
|
+
this.logger?.warn?.(`Admin social signup failed - phone constraint violation: ${dto.phone}`);
|
|
731
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
|
|
732
|
+
}
|
|
733
|
+
else if (dbError.detail?.includes('provider') && dbError.detail?.includes('providerId')) {
|
|
734
|
+
this.logger?.warn?.(`Admin social signup failed - social account constraint violation: ${dto.provider}:${dto.providerId}`);
|
|
735
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SOCIAL_ACCOUNT_EXISTS, 'This social account is already registered');
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
this.logger?.error?.(`Admin social signup failed - database constraint violation: ${dbError.message}`);
|
|
739
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this information already exists', {
|
|
740
|
+
conflictType: 'unknown',
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// Re-throw other database errors
|
|
745
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown database error';
|
|
746
|
+
this.logger?.error?.(`Admin social signup failed - database error: ${errorMessage}`);
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
// No tokens, no challenge system, no verification emails - pure user creation with social linkage
|
|
750
|
+
// Return sanitized user object and social account confirmation
|
|
751
|
+
const userDto = user_response_dto_1.UserResponseDto.fromEntity(savedUser);
|
|
752
|
+
return {
|
|
753
|
+
user: userDto,
|
|
754
|
+
socialAccount: {
|
|
755
|
+
provider: dto.provider,
|
|
756
|
+
providerId: dto.providerId,
|
|
757
|
+
providerEmail: dto.providerEmail || null,
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
// ============================================================================
|
|
762
|
+
// Admin User Management
|
|
763
|
+
// ============================================================================
|
|
764
|
+
/**
|
|
765
|
+
* Administrative user deletion with complete cascade cleanup
|
|
766
|
+
*
|
|
767
|
+
* HARD DELETE - Permanently removes user and ALL associated data including:
|
|
768
|
+
* - Sessions, verification tokens, MFA devices, trusted devices
|
|
769
|
+
* - Social accounts, login attempts, challenge sessions, audit logs
|
|
770
|
+
*
|
|
771
|
+
* Security:
|
|
772
|
+
* - NO built-in authentication - endpoint MUST be protected by admin guards
|
|
773
|
+
* - Records admin action in separate audit log (not deleted with user)
|
|
774
|
+
* - Irreversible operation - all data permanently removed
|
|
775
|
+
*
|
|
776
|
+
* @param dto - User sub to delete
|
|
777
|
+
* @returns Deletion confirmation with cascade counts
|
|
778
|
+
* @throws {NAuthException} USER_NOT_FOUND
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* ```typescript
|
|
782
|
+
* const result = await authService.deleteUser({ sub: 'user-uuid-123' });
|
|
783
|
+
* console.log(`Deleted user: ${result.deletedUserId}`);
|
|
784
|
+
* console.log(`Deleted ${result.deletedRecords.sessions} sessions`);
|
|
785
|
+
* ```
|
|
786
|
+
*/
|
|
787
|
+
async deleteUser(dto) {
|
|
788
|
+
// Ensure DTO is validated
|
|
789
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(delete_user_dto_1.DeleteUserDTO, dto);
|
|
790
|
+
// Get client info for audit
|
|
791
|
+
const clientInfo = this.clientInfoService.get();
|
|
792
|
+
this.logger?.log?.(`Admin deleteUser initiated for sub: ${dto.sub}`);
|
|
793
|
+
// Find user by sub
|
|
794
|
+
const user = await this.userRepository.findOne({ where: { sub: dto.sub } });
|
|
795
|
+
if (!user) {
|
|
796
|
+
this.logger?.warn?.(`User not found for deletion: ${dto.sub}`);
|
|
797
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found');
|
|
798
|
+
}
|
|
799
|
+
this.logger?.debug?.(`Deleting user ${user.email} (id: ${user.id}, sub: ${dto.sub})`);
|
|
800
|
+
// ============================================================================
|
|
801
|
+
// Explicit Cascade Deletion (to track counts)
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// Even though database has CASCADE, we explicitly delete each table to track counts
|
|
804
|
+
// 1. Delete Sessions
|
|
805
|
+
let sessionsCount = 0;
|
|
806
|
+
if (this.sessionRepository) {
|
|
807
|
+
const result = await this.sessionRepository.delete({ userId: user.id });
|
|
808
|
+
sessionsCount = result.affected || 0;
|
|
809
|
+
this.logger?.debug?.(`Deleted ${sessionsCount} sessions for user ${dto.sub}`);
|
|
810
|
+
}
|
|
811
|
+
// 2. Delete Verification Tokens
|
|
812
|
+
let verificationTokensCount = 0;
|
|
813
|
+
if (this.verificationTokenRepository) {
|
|
814
|
+
const result = await this.verificationTokenRepository.delete({ userId: user.id });
|
|
815
|
+
verificationTokensCount = result.affected || 0;
|
|
816
|
+
this.logger?.debug?.(`Deleted ${verificationTokensCount} verification tokens for user ${dto.sub}`);
|
|
817
|
+
}
|
|
818
|
+
// 3. Delete MFA Devices
|
|
819
|
+
let mfaDevicesCount = 0;
|
|
820
|
+
if (this.mfaDeviceRepository) {
|
|
821
|
+
const result = await this.mfaDeviceRepository.delete({ userId: user.id });
|
|
822
|
+
mfaDevicesCount = result.affected || 0;
|
|
823
|
+
this.logger?.debug?.(`Deleted ${mfaDevicesCount} MFA devices for user ${dto.sub}`);
|
|
824
|
+
}
|
|
825
|
+
// 4. Delete Trusted Devices
|
|
826
|
+
let trustedDevicesCount = 0;
|
|
827
|
+
if (this.trustedDeviceRepository) {
|
|
828
|
+
const result = await this.trustedDeviceRepository.delete({ userId: user.id });
|
|
829
|
+
trustedDevicesCount = result.affected || 0;
|
|
830
|
+
this.logger?.debug?.(`Deleted ${trustedDevicesCount} trusted devices for user ${dto.sub}`);
|
|
831
|
+
}
|
|
832
|
+
// 5. Delete Social Accounts
|
|
833
|
+
let socialAccountsCount = 0;
|
|
834
|
+
if (this.socialAccountRepository) {
|
|
835
|
+
const result = await this.socialAccountRepository.delete({ userId: user.id });
|
|
836
|
+
socialAccountsCount = result.affected || 0;
|
|
837
|
+
this.logger?.debug?.(`Deleted ${socialAccountsCount} social accounts for user ${dto.sub}`);
|
|
838
|
+
}
|
|
839
|
+
// 6. Delete Login Attempts
|
|
840
|
+
let loginAttemptsCount = 0;
|
|
841
|
+
const loginAttemptResult = await this.loginAttemptRepository.delete({ userId: user.id });
|
|
842
|
+
loginAttemptsCount = loginAttemptResult.affected || 0;
|
|
843
|
+
this.logger?.debug?.(`Deleted ${loginAttemptsCount} login attempts for user ${dto.sub}`);
|
|
844
|
+
// 7. Delete Challenge Sessions
|
|
845
|
+
let challengeSessionsCount = 0;
|
|
846
|
+
if (this.challengeSessionRepository) {
|
|
847
|
+
const result = await this.challengeSessionRepository.delete({ userId: user.id });
|
|
848
|
+
challengeSessionsCount = result.affected || 0;
|
|
849
|
+
this.logger?.debug?.(`Deleted ${challengeSessionsCount} challenge sessions for user ${dto.sub}`);
|
|
850
|
+
}
|
|
851
|
+
// 8. Delete Audit Logs (user-specific)
|
|
852
|
+
let auditLogsCount = 0;
|
|
853
|
+
if (this.authAuditRepository) {
|
|
854
|
+
const result = await this.authAuditRepository.delete({ userId: user.id });
|
|
855
|
+
auditLogsCount = result.affected || 0;
|
|
856
|
+
this.logger?.debug?.(`Deleted ${auditLogsCount} audit logs for user ${dto.sub}`);
|
|
857
|
+
}
|
|
858
|
+
// ============================================================================
|
|
859
|
+
// Record Admin Action (BEFORE deleting user to satisfy foreign key constraint)
|
|
860
|
+
// ============================================================================
|
|
861
|
+
try {
|
|
862
|
+
await this.auditService?.recordEvent({
|
|
863
|
+
userId: user.id,
|
|
864
|
+
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_DELETED,
|
|
865
|
+
eventStatus: 'INFO',
|
|
866
|
+
authMethod: 'admin',
|
|
867
|
+
metadata: {
|
|
868
|
+
deletedEmail: user.email,
|
|
869
|
+
deletedSub: dto.sub,
|
|
870
|
+
adminIdentifier: clientInfo.ipAddress || 'unknown',
|
|
871
|
+
deletedRecords: {
|
|
872
|
+
sessions: sessionsCount,
|
|
873
|
+
verificationTokens: verificationTokensCount,
|
|
874
|
+
mfaDevices: mfaDevicesCount,
|
|
875
|
+
trustedDevices: trustedDevicesCount,
|
|
876
|
+
socialAccounts: socialAccountsCount,
|
|
877
|
+
loginAttempts: loginAttemptsCount,
|
|
878
|
+
challengeSessions: challengeSessionsCount,
|
|
879
|
+
auditLogs: auditLogsCount,
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
catch (auditError) {
|
|
885
|
+
// Non-blocking: Log but continue
|
|
886
|
+
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
887
|
+
this.logger?.error?.(`Failed to record ACCOUNT_DELETED audit event: ${errorMessage}`);
|
|
888
|
+
}
|
|
889
|
+
// 9. Delete User Record (final)
|
|
890
|
+
await this.userRepository.delete({ id: user.id });
|
|
891
|
+
this.logger?.log?.(`User deleted successfully: ${user.email} (sub: ${dto.sub})`);
|
|
892
|
+
return {
|
|
893
|
+
success: true,
|
|
894
|
+
deletedUserId: dto.sub,
|
|
895
|
+
deletedRecords: {
|
|
896
|
+
sessions: sessionsCount,
|
|
897
|
+
verificationTokens: verificationTokensCount,
|
|
898
|
+
mfaDevices: mfaDevicesCount,
|
|
899
|
+
trustedDevices: trustedDevicesCount,
|
|
900
|
+
socialAccounts: socialAccountsCount,
|
|
901
|
+
loginAttempts: loginAttemptsCount,
|
|
902
|
+
challengeSessions: challengeSessionsCount,
|
|
903
|
+
auditLogs: auditLogsCount,
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Get paginated list of users with advanced filtering
|
|
909
|
+
*
|
|
910
|
+
* Supports pagination, boolean filters, exact match filters,
|
|
911
|
+
* date filters with operators (gt, gte, lt, lte, eq), and flexible sorting.
|
|
912
|
+
*
|
|
913
|
+
* Security:
|
|
914
|
+
* - NO built-in authentication - endpoint MUST be protected by admin guards
|
|
915
|
+
* - Returns sanitized user data (no passwordHash, secrets)
|
|
916
|
+
*
|
|
917
|
+
* @param dto - Filters, pagination, sorting
|
|
918
|
+
* @returns Paginated user list with metadata
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```typescript
|
|
922
|
+
* const result = await authService.getUsers({
|
|
923
|
+
* page: 1,
|
|
924
|
+
* limit: 20,
|
|
925
|
+
* isEmailVerified: true,
|
|
926
|
+
* hasSocialAuth: true,
|
|
927
|
+
* createdAt: { operator: 'gte', value: new Date('2024-01-01') },
|
|
928
|
+
* sortBy: 'createdAt',
|
|
929
|
+
* sortOrder: 'DESC'
|
|
930
|
+
* });
|
|
931
|
+
* ```
|
|
932
|
+
*/
|
|
933
|
+
async getUsers(dto) {
|
|
934
|
+
// Ensure DTO is validated
|
|
935
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(get_users_dto_1.GetUsersDTO, dto);
|
|
936
|
+
this.logger?.debug?.(`Admin getUsers initiated with filters: ${JSON.stringify(dto)}`);
|
|
937
|
+
// ============================================================================
|
|
938
|
+
// Build Query with Filters
|
|
939
|
+
// ============================================================================
|
|
940
|
+
const qb = this.userRepository.createQueryBuilder('user');
|
|
941
|
+
// Apply partial match filters (email and phone) - case-insensitive
|
|
942
|
+
// Using LOWER() for cross-database compatibility (works on both MySQL and PostgreSQL)
|
|
943
|
+
if (dto.email) {
|
|
944
|
+
qb.andWhere('LOWER(user.email) LIKE LOWER(:email)', { email: `%${dto.email}%` });
|
|
945
|
+
}
|
|
946
|
+
if (dto.phone) {
|
|
947
|
+
qb.andWhere('LOWER(user.phone) LIKE LOWER(:phone)', { phone: `%${dto.phone}%` });
|
|
948
|
+
}
|
|
949
|
+
// Apply boolean filters
|
|
950
|
+
if (dto.isEmailVerified !== undefined) {
|
|
951
|
+
qb.andWhere('user.isEmailVerified = :isEmailVerified', { isEmailVerified: dto.isEmailVerified });
|
|
952
|
+
}
|
|
953
|
+
if (dto.isPhoneVerified !== undefined) {
|
|
954
|
+
qb.andWhere('user.isPhoneVerified = :isPhoneVerified', { isPhoneVerified: dto.isPhoneVerified });
|
|
955
|
+
}
|
|
956
|
+
if (dto.hasSocialAuth !== undefined) {
|
|
957
|
+
qb.andWhere('user.hasSocialAuth = :hasSocialAuth', { hasSocialAuth: dto.hasSocialAuth });
|
|
958
|
+
}
|
|
959
|
+
if (dto.isLocked !== undefined) {
|
|
960
|
+
qb.andWhere('user.isLocked = :isLocked', { isLocked: dto.isLocked });
|
|
961
|
+
}
|
|
962
|
+
if (dto.mfaEnabled !== undefined) {
|
|
963
|
+
qb.andWhere('user.mfaEnabled = :mfaEnabled', { mfaEnabled: dto.mfaEnabled });
|
|
964
|
+
}
|
|
965
|
+
// Apply date filters with operators
|
|
966
|
+
if (dto.createdAt) {
|
|
967
|
+
const { operator, value } = dto.createdAt;
|
|
968
|
+
if (operator === 'gt') {
|
|
969
|
+
qb.andWhere('user.createdAt > :createdAtValue', { createdAtValue: value });
|
|
970
|
+
}
|
|
971
|
+
else if (operator === 'gte') {
|
|
972
|
+
qb.andWhere('user.createdAt >= :createdAtValue', { createdAtValue: value });
|
|
973
|
+
}
|
|
974
|
+
else if (operator === 'lt') {
|
|
975
|
+
qb.andWhere('user.createdAt < :createdAtValue', { createdAtValue: value });
|
|
976
|
+
}
|
|
977
|
+
else if (operator === 'lte') {
|
|
978
|
+
qb.andWhere('user.createdAt <= :createdAtValue', { createdAtValue: value });
|
|
979
|
+
}
|
|
980
|
+
else if (operator === 'eq') {
|
|
981
|
+
qb.andWhere('user.createdAt = :createdAtValue', { createdAtValue: value });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (dto.updatedAt) {
|
|
985
|
+
const { operator, value } = dto.updatedAt;
|
|
986
|
+
if (operator === 'gt') {
|
|
987
|
+
qb.andWhere('user.updatedAt > :updatedAtValue', { updatedAtValue: value });
|
|
988
|
+
}
|
|
989
|
+
else if (operator === 'gte') {
|
|
990
|
+
qb.andWhere('user.updatedAt >= :updatedAtValue', { updatedAtValue: value });
|
|
991
|
+
}
|
|
992
|
+
else if (operator === 'lt') {
|
|
993
|
+
qb.andWhere('user.updatedAt < :updatedAtValue', { updatedAtValue: value });
|
|
994
|
+
}
|
|
995
|
+
else if (operator === 'lte') {
|
|
996
|
+
qb.andWhere('user.updatedAt <= :updatedAtValue', { updatedAtValue: value });
|
|
997
|
+
}
|
|
998
|
+
else if (operator === 'eq') {
|
|
999
|
+
qb.andWhere('user.updatedAt = :updatedAtValue', { updatedAtValue: value });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
// ============================================================================
|
|
1003
|
+
// Apply Sorting
|
|
1004
|
+
// ============================================================================
|
|
1005
|
+
const sortBy = dto.sortBy || 'createdAt';
|
|
1006
|
+
const sortOrder = dto.sortOrder || 'DESC';
|
|
1007
|
+
qb.orderBy(`user.${sortBy}`, sortOrder);
|
|
1008
|
+
// ============================================================================
|
|
1009
|
+
// Apply Pagination
|
|
1010
|
+
// ============================================================================
|
|
1011
|
+
const page = dto.page || 1;
|
|
1012
|
+
const limit = dto.limit || 10;
|
|
1013
|
+
qb.skip((page - 1) * limit).take(limit);
|
|
1014
|
+
// Execute query
|
|
1015
|
+
const [users, total] = await qb.getManyAndCount();
|
|
1016
|
+
this.logger?.debug?.(`Found ${users.length} users (total: ${total}) with filters`);
|
|
1017
|
+
// Sanitize user data
|
|
1018
|
+
const sanitizedUsers = users.map((user) => user_response_dto_1.UserResponseDto.fromEntity(user));
|
|
1019
|
+
return {
|
|
1020
|
+
users: sanitizedUsers,
|
|
1021
|
+
pagination: {
|
|
1022
|
+
page,
|
|
1023
|
+
limit,
|
|
1024
|
+
total,
|
|
1025
|
+
totalPages: Math.ceil(total / limit),
|
|
1026
|
+
},
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Administrative permanent account locking
|
|
1031
|
+
*
|
|
1032
|
+
* Sets permanent lock (lockedUntil=NULL) and immediately revokes all active sessions.
|
|
1033
|
+
* Reuses existing rate-limit lock fields (isLocked, lockReason, lockedAt, lockedUntil).
|
|
1034
|
+
*
|
|
1035
|
+
* Permanent vs Temporary locks:
|
|
1036
|
+
* - Rate limiting: lockedUntil = future date (temporary auto-unlock)
|
|
1037
|
+
* - Admin disableUser: lockedUntil = NULL (permanent manual lock)
|
|
1038
|
+
*
|
|
1039
|
+
* Security:
|
|
1040
|
+
* - NO built-in authentication - endpoint MUST be protected by admin guards
|
|
1041
|
+
* - Revokes all sessions immediately (forced logout)
|
|
1042
|
+
* - Records ACCOUNT_DISABLED audit event with admin identifier
|
|
1043
|
+
*
|
|
1044
|
+
* @param dto - User sub and optional reason
|
|
1045
|
+
* @returns User object with updated lock status and revoked session count
|
|
1046
|
+
* @throws {NAuthException} USER_NOT_FOUND
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* ```typescript
|
|
1050
|
+
* const result = await authService.disableUser({
|
|
1051
|
+
* sub: 'user-uuid-123',
|
|
1052
|
+
* reason: 'Suspicious activity detected'
|
|
1053
|
+
* });
|
|
1054
|
+
* console.log(`Revoked ${result.revokedSessions} sessions`);
|
|
1055
|
+
* ```
|
|
1056
|
+
*/
|
|
1057
|
+
async disableUser(dto) {
|
|
1058
|
+
// Ensure DTO is validated
|
|
1059
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(disable_user_dto_1.DisableUserDTO, dto);
|
|
1060
|
+
// Get client info for audit
|
|
1061
|
+
const clientInfo = this.clientInfoService.get();
|
|
1062
|
+
this.logger?.log?.(`Admin disableUser initiated for sub: ${dto.sub}`);
|
|
1063
|
+
// Find user by sub
|
|
1064
|
+
const user = await this.userRepository.findOne({ where: { sub: dto.sub } });
|
|
1065
|
+
if (!user) {
|
|
1066
|
+
this.logger?.warn?.(`User not found for disabling: ${dto.sub}`);
|
|
1067
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found');
|
|
1068
|
+
}
|
|
1069
|
+
this.logger?.debug?.(`Disabling user ${user.email} (id: ${user.id}, sub: ${dto.sub})`);
|
|
1070
|
+
// ============================================================================
|
|
1071
|
+
// Set Permanent Lock (lockedUntil = NULL)
|
|
1072
|
+
// ============================================================================
|
|
1073
|
+
// Use update() to ensure persistence and avoid entity state issues
|
|
1074
|
+
await this.userRepository.update({ id: user.id }, {
|
|
1075
|
+
isLocked: true,
|
|
1076
|
+
lockReason: dto.reason || 'Account disabled',
|
|
1077
|
+
lockedAt: new Date(),
|
|
1078
|
+
lockedUntil: null, // NULL = permanent lock (vs rate-limit's future date)
|
|
1079
|
+
});
|
|
1080
|
+
// Reload user to get updated entity with lock fields
|
|
1081
|
+
const updatedUser = (await this.userRepository.findOne({ where: { id: user.id } }));
|
|
1082
|
+
if (!updatedUser) {
|
|
1083
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found after update');
|
|
1084
|
+
}
|
|
1085
|
+
this.logger?.log?.(`User locked permanently: ${updatedUser.email} (sub: ${dto.sub})`);
|
|
1086
|
+
// ============================================================================
|
|
1087
|
+
// Revoke All Sessions (force logout)
|
|
1088
|
+
// ============================================================================
|
|
1089
|
+
let revokedCount = 0;
|
|
1090
|
+
try {
|
|
1091
|
+
revokedCount = await this.sessionService.revokeAllUserSessions(updatedUser.id, 'Account disabled');
|
|
1092
|
+
this.logger?.debug?.(`Revoked ${revokedCount} sessions for user ${dto.sub}`);
|
|
1093
|
+
}
|
|
1094
|
+
catch (sessionError) {
|
|
1095
|
+
// Non-blocking: Log but continue
|
|
1096
|
+
const errorMessage = sessionError instanceof Error ? sessionError.message : 'Unknown error';
|
|
1097
|
+
this.logger?.warn?.(`Failed to revoke sessions for user ${dto.sub}: ${errorMessage}`);
|
|
1098
|
+
}
|
|
1099
|
+
// ============================================================================
|
|
1100
|
+
// Record Admin Action (ACCOUNT_DISABLED)
|
|
1101
|
+
// ============================================================================
|
|
1102
|
+
if (!this.auditService) {
|
|
1103
|
+
this.logger?.warn?.(`Audit service not available - ACCOUNT_DISABLED event not recorded for user ${dto.sub}. Enable audit logs in config.auditLogs.enabled`);
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
try {
|
|
1107
|
+
// Get admin user ID from client info (the currently logged in user performing this action)
|
|
1108
|
+
// This is extracted from the JWT token by interceptors/handlers
|
|
1109
|
+
const adminUserId = clientInfo?.userId;
|
|
1110
|
+
// Set performedBy to the admin's user ID (who locked the account)
|
|
1111
|
+
// This identifies which admin user performed the action in the audit trail
|
|
1112
|
+
const performedBy = adminUserId ? String(adminUserId) : clientInfo.ipAddress || 'system';
|
|
1113
|
+
if (adminUserId) {
|
|
1114
|
+
this.logger?.debug?.(`Admin user ID ${adminUserId} (currently logged in) is disabling account for user ${dto.sub}`);
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
this.logger?.warn?.(`No admin user ID in clientInfo - performedBy will be set to IP address or 'system' for user ${dto.sub}`);
|
|
1118
|
+
}
|
|
1119
|
+
const auditResult = await this.auditService.recordEvent({
|
|
1120
|
+
userId: updatedUser.id, // The user whose account is being disabled
|
|
1121
|
+
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_DISABLED,
|
|
1122
|
+
eventStatus: 'INFO',
|
|
1123
|
+
authMethod: 'admin',
|
|
1124
|
+
performedBy, // The admin user ID (currently logged in user) who performed this action
|
|
1125
|
+
reason: updatedUser.lockReason || 'Account disabled',
|
|
1126
|
+
description: `Account disabled by administrator. User: ${updatedUser.email} (sub: ${dto.sub}). ${revokedCount} session(s) revoked.`,
|
|
1127
|
+
metadata: {
|
|
1128
|
+
email: updatedUser.email,
|
|
1129
|
+
userSub: dto.sub,
|
|
1130
|
+
reason: updatedUser.lockReason,
|
|
1131
|
+
adminIdentifier: clientInfo.ipAddress || 'unknown',
|
|
1132
|
+
adminUserId: adminUserId || null,
|
|
1133
|
+
revokedSessions: revokedCount,
|
|
1134
|
+
lockedAt: updatedUser.lockedAt,
|
|
1135
|
+
lockedUntil: updatedUser.lockedUntil,
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
if (auditResult) {
|
|
1139
|
+
this.logger?.debug?.(`ACCOUNT_DISABLED audit event recorded successfully for user ${dto.sub}`);
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
this.logger?.warn?.(`ACCOUNT_DISABLED audit event returned null for user ${dto.sub}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
catch (auditError) {
|
|
1146
|
+
// Non-blocking: Log but continue
|
|
1147
|
+
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
1148
|
+
const errorStack = auditError instanceof Error ? auditError.stack : undefined;
|
|
1149
|
+
this.logger?.error?.(`Failed to record ACCOUNT_DISABLED audit event: ${errorMessage}`, {
|
|
1150
|
+
error: auditError,
|
|
1151
|
+
errorStack,
|
|
1152
|
+
userId: updatedUser.id,
|
|
1153
|
+
userSub: dto.sub,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
// Return sanitized user and revoked session count
|
|
1158
|
+
const userDto = user_response_dto_1.UserResponseDto.fromEntity(updatedUser);
|
|
1159
|
+
return {
|
|
1160
|
+
success: true,
|
|
1161
|
+
user: userDto,
|
|
1162
|
+
revokedSessions: revokedCount,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Enable (unlock) user account
|
|
1167
|
+
*
|
|
1168
|
+
* Unlocks a previously locked user account by clearing all lock fields.
|
|
1169
|
+
* This reverses the effect of disableUser() or rate-limit lockouts.
|
|
1170
|
+
*
|
|
1171
|
+
* Security:
|
|
1172
|
+
* - NO built-in authentication - endpoint MUST be protected by admin guards
|
|
1173
|
+
* - Clears lock fields (isLocked, lockReason, lockedAt, lockedUntil)
|
|
1174
|
+
* - Resets failed login attempts counter
|
|
1175
|
+
* - Records ACCOUNT_ENABLED audit event with admin identifier
|
|
1176
|
+
*
|
|
1177
|
+
* @param dto - User sub to enable
|
|
1178
|
+
* @returns User object with updated lock status
|
|
1179
|
+
* @throws {NAuthException} USER_NOT_FOUND
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* ```typescript
|
|
1183
|
+
* const result = await authService.enableUser({
|
|
1184
|
+
* sub: 'user-uuid-123'
|
|
1185
|
+
* });
|
|
1186
|
+
* console.log(`User unlocked: ${result.user.email}`);
|
|
1187
|
+
* ```
|
|
1188
|
+
*/
|
|
1189
|
+
async enableUser(dto) {
|
|
1190
|
+
// Ensure DTO is validated
|
|
1191
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(enable_user_dto_1.EnableUserDTO, dto);
|
|
1192
|
+
// Get client info for audit
|
|
1193
|
+
const clientInfo = this.clientInfoService.get();
|
|
1194
|
+
this.logger?.log?.(`Admin enableUser initiated for sub: ${dto.sub}`);
|
|
1195
|
+
// Find user by sub
|
|
1196
|
+
const user = await this.userRepository.findOne({ where: { sub: dto.sub } });
|
|
1197
|
+
if (!user) {
|
|
1198
|
+
this.logger?.warn?.(`User not found for enabling: ${dto.sub}`);
|
|
1199
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found');
|
|
1200
|
+
}
|
|
1201
|
+
this.logger?.debug?.(`Enabling user ${user.email} (id: ${user.id}, sub: ${dto.sub})`);
|
|
1202
|
+
// ============================================================================
|
|
1203
|
+
// Clear Lock Fields (unlock account)
|
|
1204
|
+
// ============================================================================
|
|
1205
|
+
await this.userRepository.update({ id: user.id }, {
|
|
1206
|
+
isLocked: false,
|
|
1207
|
+
lockReason: null,
|
|
1208
|
+
lockedAt: null,
|
|
1209
|
+
lockedUntil: null,
|
|
1210
|
+
failedLoginAttempts: 0, // Reset failed attempts counter
|
|
1211
|
+
});
|
|
1212
|
+
// Reload user to get updated entity
|
|
1213
|
+
const updatedUser = (await this.userRepository.findOne({ where: { id: user.id } }));
|
|
1214
|
+
if (!updatedUser) {
|
|
1215
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found after update');
|
|
1216
|
+
}
|
|
1217
|
+
this.logger?.log?.(`User unlocked: ${updatedUser.email} (sub: ${dto.sub})`);
|
|
1218
|
+
// ============================================================================
|
|
1219
|
+
// Record Admin Action (ACCOUNT_ENABLED)
|
|
1220
|
+
// ============================================================================
|
|
1221
|
+
if (!this.auditService) {
|
|
1222
|
+
this.logger?.warn?.(`Audit service not available - ACCOUNT_ENABLED event not recorded for user ${dto.sub}. Enable audit logs in config.auditLogs.enabled`);
|
|
1223
|
+
}
|
|
1224
|
+
else {
|
|
1225
|
+
try {
|
|
1226
|
+
// Get admin user ID from client info (the currently logged in user performing this action)
|
|
1227
|
+
const adminUserId = clientInfo?.userId;
|
|
1228
|
+
// Set performedBy to the admin's user ID (who unlocked the account)
|
|
1229
|
+
const performedBy = adminUserId ? String(adminUserId) : clientInfo.ipAddress || 'system';
|
|
1230
|
+
if (adminUserId) {
|
|
1231
|
+
this.logger?.debug?.(`Admin user ID ${adminUserId} (currently logged in) is enabling account for user ${dto.sub}`);
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
this.logger?.warn?.(`No admin user ID in clientInfo - performedBy will be set to IP address or 'system' for user ${dto.sub}`);
|
|
1235
|
+
}
|
|
1236
|
+
const auditResult = await this.auditService.recordEvent({
|
|
1237
|
+
userId: updatedUser.id,
|
|
1238
|
+
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_ENABLED,
|
|
1239
|
+
eventStatus: 'INFO',
|
|
1240
|
+
authMethod: 'admin',
|
|
1241
|
+
performedBy,
|
|
1242
|
+
reason: 'admin_unlock',
|
|
1243
|
+
description: 'Account unlocked by administrator',
|
|
1244
|
+
metadata: {
|
|
1245
|
+
userSub: dto.sub,
|
|
1246
|
+
adminIdentifier: clientInfo.ipAddress || 'unknown',
|
|
1247
|
+
adminUserId: adminUserId || null,
|
|
1248
|
+
previousLockReason: user.lockReason,
|
|
1249
|
+
previousLockedAt: user.lockedAt,
|
|
1250
|
+
previousLockedUntil: user.lockedUntil,
|
|
1251
|
+
},
|
|
1252
|
+
});
|
|
1253
|
+
if (auditResult) {
|
|
1254
|
+
this.logger?.debug?.(`ACCOUNT_ENABLED audit event recorded successfully for user ${dto.sub}`);
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
this.logger?.warn?.(`ACCOUNT_ENABLED audit event returned null for user ${dto.sub}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
catch (auditError) {
|
|
1261
|
+
// Non-blocking: Log but continue
|
|
1262
|
+
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
1263
|
+
const errorStack = auditError instanceof Error ? auditError.stack : undefined;
|
|
1264
|
+
this.logger?.error?.(`Failed to record ACCOUNT_ENABLED audit event: ${errorMessage}`, {
|
|
1265
|
+
error: auditError,
|
|
1266
|
+
errorStack,
|
|
1267
|
+
userId: updatedUser.id,
|
|
1268
|
+
userSub: dto.sub,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
// Return sanitized user
|
|
1273
|
+
const userDto = user_response_dto_1.UserResponseDto.fromEntity(updatedUser);
|
|
1274
|
+
return {
|
|
1275
|
+
success: true,
|
|
1276
|
+
user: userDto,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
// ============================================================================
|
|
507
1280
|
// User Login
|
|
508
1281
|
// ============================================================================
|
|
509
1282
|
/**
|
|
@@ -665,6 +1438,89 @@ class AuthService {
|
|
|
665
1438
|
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INVALID_CREDENTIALS, 'Invalid credentials');
|
|
666
1439
|
}
|
|
667
1440
|
// ============================================================================
|
|
1441
|
+
// Account Lock Check (Admin Disabled / Rate Limit Lockout)
|
|
1442
|
+
// ============================================================================
|
|
1443
|
+
// Check if account is permanently locked (lockedUntil = NULL) or temporarily locked (lockedUntil > now)
|
|
1444
|
+
if (user.isLocked) {
|
|
1445
|
+
const now = new Date();
|
|
1446
|
+
const isPermanentlyLocked = user.lockedUntil === null;
|
|
1447
|
+
const isTemporarilyLocked = user.lockedUntil && new Date(user.lockedUntil) > now;
|
|
1448
|
+
if (isPermanentlyLocked || isTemporarilyLocked) {
|
|
1449
|
+
const lockReason = user.lockReason || 'Account is locked';
|
|
1450
|
+
this.logger?.warn?.(`Login blocked - account locked for user: ${user.email} (sub: ${user.sub}). Reason: ${lockReason}`);
|
|
1451
|
+
// Record blocked login attempt
|
|
1452
|
+
await this.recordLoginAttempt(dto.identifier, false, 'account_locked');
|
|
1453
|
+
// ============================================================================
|
|
1454
|
+
// Audit: Record blocked login (account locked)
|
|
1455
|
+
// ============================================================================
|
|
1456
|
+
if (fireAndForget) {
|
|
1457
|
+
this.auditService
|
|
1458
|
+
?.recordEvent({
|
|
1459
|
+
userId: user.id,
|
|
1460
|
+
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.LOGIN_BLOCKED,
|
|
1461
|
+
eventStatus: 'FAILURE',
|
|
1462
|
+
authMethod: 'password',
|
|
1463
|
+
reason: 'account_locked',
|
|
1464
|
+
description: `Login blocked - account locked: ${lockReason}`,
|
|
1465
|
+
metadata: {
|
|
1466
|
+
lockReason: user.lockReason,
|
|
1467
|
+
lockedAt: user.lockedAt,
|
|
1468
|
+
lockedUntil: user.lockedUntil,
|
|
1469
|
+
isPermanent: isPermanentlyLocked,
|
|
1470
|
+
},
|
|
1471
|
+
})
|
|
1472
|
+
.catch((err) => {
|
|
1473
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
1474
|
+
this.logger?.error?.(`Failed to record LOGIN_BLOCKED audit event (fire-and-forget): ${errorMessage}`, {
|
|
1475
|
+
error: err,
|
|
1476
|
+
userId: user.id,
|
|
1477
|
+
userSub: user.sub,
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
try {
|
|
1483
|
+
await this.auditService?.recordEvent({
|
|
1484
|
+
userId: user.id,
|
|
1485
|
+
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.LOGIN_BLOCKED,
|
|
1486
|
+
eventStatus: 'FAILURE',
|
|
1487
|
+
authMethod: 'password',
|
|
1488
|
+
reason: 'account_locked',
|
|
1489
|
+
description: `Login blocked - account locked: ${lockReason}`,
|
|
1490
|
+
metadata: {
|
|
1491
|
+
lockReason: user.lockReason,
|
|
1492
|
+
lockedAt: user.lockedAt,
|
|
1493
|
+
lockedUntil: user.lockedUntil,
|
|
1494
|
+
isPermanent: isPermanentlyLocked,
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
catch (auditError) {
|
|
1499
|
+
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
1500
|
+
this.logger?.error?.(`Failed to record LOGIN_BLOCKED audit event (account locked): ${errorMessage}`, {
|
|
1501
|
+
error: auditError,
|
|
1502
|
+
userId: user.id,
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.ACCOUNT_LOCKED, lockReason, {
|
|
1507
|
+
lockReason: user.lockReason,
|
|
1508
|
+
lockedAt: user.lockedAt,
|
|
1509
|
+
lockedUntil: user.lockedUntil,
|
|
1510
|
+
isPermanent: isPermanentlyLocked,
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
// Account was temporarily locked but lock has expired - unlock it
|
|
1515
|
+
this.logger?.debug?.(`Account lock expired for user: ${user.email} (sub: ${user.sub}), unlocking account`);
|
|
1516
|
+
user.isLocked = false;
|
|
1517
|
+
user.lockReason = null;
|
|
1518
|
+
user.lockedAt = null;
|
|
1519
|
+
user.lockedUntil = null;
|
|
1520
|
+
await this.userRepository.save(user);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
// ============================================================================
|
|
668
1524
|
// Password Expiry Check
|
|
669
1525
|
// ============================================================================
|
|
670
1526
|
const expiryDays = this.config.password?.expiryDays;
|
|
@@ -1910,7 +2766,11 @@ class AuthService {
|
|
|
1910
2766
|
switch (challengeSession.challengeName) {
|
|
1911
2767
|
case auth_challenge_dto_1.AuthChallenge.VERIFY_EMAIL: {
|
|
1912
2768
|
// Resend email verification
|
|
1913
|
-
|
|
2769
|
+
// Pass challengeSessionId to ensure new token is linked to this challenge session
|
|
2770
|
+
const resendDto = Object.assign(new verify_email_dto_1.ResendVerificationEmailDTO(), {
|
|
2771
|
+
sub: user.sub,
|
|
2772
|
+
challengeSessionId: challengeSession.id,
|
|
2773
|
+
});
|
|
1914
2774
|
await this.emailVerificationService.resendVerificationEmail(resendDto);
|
|
1915
2775
|
const maskedEmail = this.maskEmail(user.email);
|
|
1916
2776
|
this.logger?.debug?.(`Email verification code resent: user=${user.sub}, email=${maskedEmail}`);
|
|
@@ -3220,6 +4080,11 @@ class AuthService {
|
|
|
3220
4080
|
'user.isEmailVerified',
|
|
3221
4081
|
'user.isPhoneVerified',
|
|
3222
4082
|
'user.mfaExempt', // Required for MFA exemption check in challenge flow
|
|
4083
|
+
// Lock fields - required for account lock check in login flow
|
|
4084
|
+
'user.isLocked',
|
|
4085
|
+
'user.lockReason',
|
|
4086
|
+
'user.lockedAt',
|
|
4087
|
+
'user.lockedUntil',
|
|
3223
4088
|
// The following are used for messaging/challenge determination when needed
|
|
3224
4089
|
'user.socialProviders',
|
|
3225
4090
|
'user.backupCodes',
|