@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.
Files changed (63) hide show
  1. package/dist/dto/admin-signup-social.dto.d.ts +257 -0
  2. package/dist/dto/admin-signup-social.dto.d.ts.map +1 -0
  3. package/dist/dto/admin-signup-social.dto.js +389 -0
  4. package/dist/dto/admin-signup-social.dto.js.map +1 -0
  5. package/dist/dto/delete-user.dto.d.ts +71 -0
  6. package/dist/dto/delete-user.dto.d.ts.map +1 -0
  7. package/dist/dto/delete-user.dto.js +82 -0
  8. package/dist/dto/delete-user.dto.js.map +1 -0
  9. package/dist/dto/disable-user.dto.d.ts +61 -0
  10. package/dist/dto/disable-user.dto.d.ts.map +1 -0
  11. package/dist/dto/disable-user.dto.js +86 -0
  12. package/dist/dto/disable-user.dto.js.map +1 -0
  13. package/dist/dto/enable-user.dto.d.ts +44 -0
  14. package/dist/dto/enable-user.dto.d.ts.map +1 -0
  15. package/dist/dto/enable-user.dto.js +63 -0
  16. package/dist/dto/enable-user.dto.js.map +1 -0
  17. package/dist/dto/get-users.dto.d.ts +154 -0
  18. package/dist/dto/get-users.dto.d.ts.map +1 -0
  19. package/dist/dto/get-users.dto.js +250 -0
  20. package/dist/dto/get-users.dto.js.map +1 -0
  21. package/dist/dto/index.d.ts +5 -0
  22. package/dist/dto/index.d.ts.map +1 -1
  23. package/dist/dto/index.js +5 -0
  24. package/dist/dto/index.js.map +1 -1
  25. package/dist/dto/user-response.dto.d.ts +5 -0
  26. package/dist/dto/user-response.dto.d.ts.map +1 -1
  27. package/dist/dto/user-response.dto.js +6 -0
  28. package/dist/dto/user-response.dto.js.map +1 -1
  29. package/dist/dto/verify-email.dto.d.ts +10 -0
  30. package/dist/dto/verify-email.dto.d.ts.map +1 -1
  31. package/dist/dto/verify-email.dto.js +16 -0
  32. package/dist/dto/verify-email.dto.js.map +1 -1
  33. package/dist/entities/user.entity.d.ts +18 -2
  34. package/dist/entities/user.entity.d.ts.map +1 -1
  35. package/dist/entities/user.entity.js +18 -2
  36. package/dist/entities/user.entity.js.map +1 -1
  37. package/dist/enums/auth-audit-event-type.enum.d.ts +5 -0
  38. package/dist/enums/auth-audit-event-type.enum.d.ts.map +1 -1
  39. package/dist/enums/auth-audit-event-type.enum.js +5 -0
  40. package/dist/enums/auth-audit-event-type.enum.js.map +1 -1
  41. package/dist/enums/error-codes.enum.d.ts +13 -0
  42. package/dist/enums/error-codes.enum.d.ts.map +1 -1
  43. package/dist/enums/error-codes.enum.js +13 -0
  44. package/dist/enums/error-codes.enum.js.map +1 -1
  45. package/dist/services/auth.service.d.ts +172 -2
  46. package/dist/services/auth.service.d.ts.map +1 -1
  47. package/dist/services/auth.service.js +867 -2
  48. package/dist/services/auth.service.js.map +1 -1
  49. package/dist/services/email-verification.service.d.ts.map +1 -1
  50. package/dist/services/email-verification.service.js +7 -7
  51. package/dist/services/email-verification.service.js.map +1 -1
  52. package/dist/services/social-auth-base.service.d.ts +5 -1
  53. package/dist/services/social-auth-base.service.d.ts.map +1 -1
  54. package/dist/services/social-auth-base.service.js +62 -2
  55. package/dist/services/social-auth-base.service.js.map +1 -1
  56. package/dist/services/social-auth.service.d.ts +2 -1
  57. package/dist/services/social-auth.service.d.ts.map +1 -1
  58. package/dist/services/social-auth.service.js +5 -1
  59. package/dist/services/social-auth.service.js.map +1 -1
  60. package/dist/utils/setup/init-services.d.ts.map +1 -1
  61. package/dist/utils/setup/init-services.js +2 -1
  62. package/dist/utils/setup/init-services.js.map +1 -1
  63. 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
- const resendDto = Object.assign(new verify_email_dto_1.ResendVerificationEmailDTO(), { sub: user.sub });
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',