@nauth-toolkit/core 0.1.32 → 0.1.33

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 +862 -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,748 @@ 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 and socialProviders will be updated by SocialAuthService.createOrUpdateSocialAccount()
668
+ });
669
+ let savedUser;
670
+ try {
671
+ savedUser = (await this.userRepository.save(user));
672
+ this.logger?.log?.(`Admin social user created successfully: ${dto.email} (sub: ${savedUser.sub}, provider: ${dto.provider})`);
673
+ // Create social account linkage
674
+ this.logger?.debug?.(`Creating social account linkage: ${dto.provider}:${dto.providerId}`);
675
+ await this.socialAuthService.createOrUpdateSocialAccount(savedUser.id, dto.provider, dto.providerId, dto.providerEmail || null, dto.socialMetadata);
676
+ this.logger?.log?.(`Social account linked successfully: ${dto.provider}:${dto.providerId}`);
677
+ // ============================================================================
678
+ // Audit: Record account creation by admin (social import)
679
+ // ============================================================================
680
+ try {
681
+ await this.auditService?.recordEvent({
682
+ userId: savedUser.id,
683
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_CREATED,
684
+ eventStatus: 'INFO',
685
+ authMethod: 'admin-social',
686
+ // Client info automatically included from context
687
+ metadata: {
688
+ email: savedUser.email,
689
+ username: savedUser.username || null,
690
+ createdByAdmin: true,
691
+ adminIdentifier: clientInfo.ipAddress || 'unknown',
692
+ isEmailVerified: savedUser.isEmailVerified,
693
+ isPhoneVerified: savedUser.isPhoneVerified,
694
+ mustChangePassword: savedUser.mustChangePassword,
695
+ provider: dto.provider,
696
+ providerId: dto.providerId,
697
+ hasPassword: !!dto.password,
698
+ socialImport: true,
699
+ },
700
+ });
701
+ }
702
+ catch (auditError) {
703
+ // Non-blocking: Log but continue
704
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
705
+ this.logger?.error?.(`Failed to record ACCOUNT_CREATED audit event: ${errorMessage}`, {
706
+ error: auditError,
707
+ userId: savedUser.id,
708
+ });
709
+ }
710
+ }
711
+ catch (error) {
712
+ // Handle database constraint violations gracefully
713
+ if (error && typeof error === 'object' && 'code' in error && error.code === '23505') {
714
+ // PostgreSQL unique constraint violation
715
+ const dbError = error;
716
+ if (dbError.detail?.includes('email')) {
717
+ this.logger?.warn?.(`Admin social signup failed - email constraint violation: ${dto.email}`);
718
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
719
+ }
720
+ else if (dbError.detail?.includes('username')) {
721
+ this.logger?.warn?.(`Admin social signup failed - username constraint violation: ${dto.username}`);
722
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
723
+ }
724
+ else if (dbError.detail?.includes('phone')) {
725
+ this.logger?.warn?.(`Admin social signup failed - phone constraint violation: ${dto.phone}`);
726
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
727
+ }
728
+ else if (dbError.detail?.includes('provider') && dbError.detail?.includes('providerId')) {
729
+ this.logger?.warn?.(`Admin social signup failed - social account constraint violation: ${dto.provider}:${dto.providerId}`);
730
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SOCIAL_ACCOUNT_EXISTS, 'This social account is already registered');
731
+ }
732
+ else {
733
+ this.logger?.error?.(`Admin social signup failed - database constraint violation: ${dbError.message}`);
734
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this information already exists', {
735
+ conflictType: 'unknown',
736
+ });
737
+ }
738
+ }
739
+ // Re-throw other database errors
740
+ const errorMessage = error instanceof Error ? error.message : 'Unknown database error';
741
+ this.logger?.error?.(`Admin social signup failed - database error: ${errorMessage}`);
742
+ throw error;
743
+ }
744
+ // No tokens, no challenge system, no verification emails - pure user creation with social linkage
745
+ // Return sanitized user object and social account confirmation
746
+ const userDto = user_response_dto_1.UserResponseDto.fromEntity(savedUser);
747
+ return {
748
+ user: userDto,
749
+ socialAccount: {
750
+ provider: dto.provider,
751
+ providerId: dto.providerId,
752
+ providerEmail: dto.providerEmail || null,
753
+ },
754
+ };
755
+ }
756
+ // ============================================================================
757
+ // Admin User Management
758
+ // ============================================================================
759
+ /**
760
+ * Administrative user deletion with complete cascade cleanup
761
+ *
762
+ * HARD DELETE - Permanently removes user and ALL associated data including:
763
+ * - Sessions, verification tokens, MFA devices, trusted devices
764
+ * - Social accounts, login attempts, challenge sessions, audit logs
765
+ *
766
+ * Security:
767
+ * - NO built-in authentication - endpoint MUST be protected by admin guards
768
+ * - Records admin action in separate audit log (not deleted with user)
769
+ * - Irreversible operation - all data permanently removed
770
+ *
771
+ * @param dto - User sub to delete
772
+ * @returns Deletion confirmation with cascade counts
773
+ * @throws {NAuthException} USER_NOT_FOUND
774
+ *
775
+ * @example
776
+ * ```typescript
777
+ * const result = await authService.deleteUser({ sub: 'user-uuid-123' });
778
+ * console.log(`Deleted user: ${result.deletedUserId}`);
779
+ * console.log(`Deleted ${result.deletedRecords.sessions} sessions`);
780
+ * ```
781
+ */
782
+ async deleteUser(dto) {
783
+ // Ensure DTO is validated
784
+ dto = await (0, dto_validator_1.ensureValidatedDto)(delete_user_dto_1.DeleteUserDTO, dto);
785
+ // Get client info for audit
786
+ const clientInfo = this.clientInfoService.get();
787
+ this.logger?.log?.(`Admin deleteUser initiated for sub: ${dto.sub}`);
788
+ // Find user by sub
789
+ const user = await this.userRepository.findOne({ where: { sub: dto.sub } });
790
+ if (!user) {
791
+ this.logger?.warn?.(`User not found for deletion: ${dto.sub}`);
792
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found');
793
+ }
794
+ this.logger?.debug?.(`Deleting user ${user.email} (id: ${user.id}, sub: ${dto.sub})`);
795
+ // ============================================================================
796
+ // Explicit Cascade Deletion (to track counts)
797
+ // ============================================================================
798
+ // Even though database has CASCADE, we explicitly delete each table to track counts
799
+ // 1. Delete Sessions
800
+ let sessionsCount = 0;
801
+ if (this.sessionRepository) {
802
+ const result = await this.sessionRepository.delete({ userId: user.id });
803
+ sessionsCount = result.affected || 0;
804
+ this.logger?.debug?.(`Deleted ${sessionsCount} sessions for user ${dto.sub}`);
805
+ }
806
+ // 2. Delete Verification Tokens
807
+ let verificationTokensCount = 0;
808
+ if (this.verificationTokenRepository) {
809
+ const result = await this.verificationTokenRepository.delete({ userId: user.id });
810
+ verificationTokensCount = result.affected || 0;
811
+ this.logger?.debug?.(`Deleted ${verificationTokensCount} verification tokens for user ${dto.sub}`);
812
+ }
813
+ // 3. Delete MFA Devices
814
+ let mfaDevicesCount = 0;
815
+ if (this.mfaDeviceRepository) {
816
+ const result = await this.mfaDeviceRepository.delete({ userId: user.id });
817
+ mfaDevicesCount = result.affected || 0;
818
+ this.logger?.debug?.(`Deleted ${mfaDevicesCount} MFA devices for user ${dto.sub}`);
819
+ }
820
+ // 4. Delete Trusted Devices
821
+ let trustedDevicesCount = 0;
822
+ if (this.trustedDeviceRepository) {
823
+ const result = await this.trustedDeviceRepository.delete({ userId: user.id });
824
+ trustedDevicesCount = result.affected || 0;
825
+ this.logger?.debug?.(`Deleted ${trustedDevicesCount} trusted devices for user ${dto.sub}`);
826
+ }
827
+ // 5. Delete Social Accounts
828
+ let socialAccountsCount = 0;
829
+ if (this.socialAccountRepository) {
830
+ const result = await this.socialAccountRepository.delete({ userId: user.id });
831
+ socialAccountsCount = result.affected || 0;
832
+ this.logger?.debug?.(`Deleted ${socialAccountsCount} social accounts for user ${dto.sub}`);
833
+ }
834
+ // 6. Delete Login Attempts
835
+ let loginAttemptsCount = 0;
836
+ const loginAttemptResult = await this.loginAttemptRepository.delete({ userId: user.id });
837
+ loginAttemptsCount = loginAttemptResult.affected || 0;
838
+ this.logger?.debug?.(`Deleted ${loginAttemptsCount} login attempts for user ${dto.sub}`);
839
+ // 7. Delete Challenge Sessions
840
+ let challengeSessionsCount = 0;
841
+ if (this.challengeSessionRepository) {
842
+ const result = await this.challengeSessionRepository.delete({ userId: user.id });
843
+ challengeSessionsCount = result.affected || 0;
844
+ this.logger?.debug?.(`Deleted ${challengeSessionsCount} challenge sessions for user ${dto.sub}`);
845
+ }
846
+ // 8. Delete Audit Logs (user-specific)
847
+ let auditLogsCount = 0;
848
+ if (this.authAuditRepository) {
849
+ const result = await this.authAuditRepository.delete({ userId: user.id });
850
+ auditLogsCount = result.affected || 0;
851
+ this.logger?.debug?.(`Deleted ${auditLogsCount} audit logs for user ${dto.sub}`);
852
+ }
853
+ // ============================================================================
854
+ // Record Admin Action (BEFORE deleting user to satisfy foreign key constraint)
855
+ // ============================================================================
856
+ try {
857
+ await this.auditService?.recordEvent({
858
+ userId: user.id,
859
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_DELETED,
860
+ eventStatus: 'INFO',
861
+ authMethod: 'admin',
862
+ metadata: {
863
+ deletedEmail: user.email,
864
+ deletedSub: dto.sub,
865
+ adminIdentifier: clientInfo.ipAddress || 'unknown',
866
+ deletedRecords: {
867
+ sessions: sessionsCount,
868
+ verificationTokens: verificationTokensCount,
869
+ mfaDevices: mfaDevicesCount,
870
+ trustedDevices: trustedDevicesCount,
871
+ socialAccounts: socialAccountsCount,
872
+ loginAttempts: loginAttemptsCount,
873
+ challengeSessions: challengeSessionsCount,
874
+ auditLogs: auditLogsCount,
875
+ },
876
+ },
877
+ });
878
+ }
879
+ catch (auditError) {
880
+ // Non-blocking: Log but continue
881
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
882
+ this.logger?.error?.(`Failed to record ACCOUNT_DELETED audit event: ${errorMessage}`);
883
+ }
884
+ // 9. Delete User Record (final)
885
+ await this.userRepository.delete({ id: user.id });
886
+ this.logger?.log?.(`User deleted successfully: ${user.email} (sub: ${dto.sub})`);
887
+ return {
888
+ success: true,
889
+ deletedUserId: dto.sub,
890
+ deletedRecords: {
891
+ sessions: sessionsCount,
892
+ verificationTokens: verificationTokensCount,
893
+ mfaDevices: mfaDevicesCount,
894
+ trustedDevices: trustedDevicesCount,
895
+ socialAccounts: socialAccountsCount,
896
+ loginAttempts: loginAttemptsCount,
897
+ challengeSessions: challengeSessionsCount,
898
+ auditLogs: auditLogsCount,
899
+ },
900
+ };
901
+ }
902
+ /**
903
+ * Get paginated list of users with advanced filtering
904
+ *
905
+ * Supports pagination, boolean filters, exact match filters,
906
+ * date filters with operators (gt, gte, lt, lte, eq), and flexible sorting.
907
+ *
908
+ * Security:
909
+ * - NO built-in authentication - endpoint MUST be protected by admin guards
910
+ * - Returns sanitized user data (no passwordHash, secrets)
911
+ *
912
+ * @param dto - Filters, pagination, sorting
913
+ * @returns Paginated user list with metadata
914
+ *
915
+ * @example
916
+ * ```typescript
917
+ * const result = await authService.getUsers({
918
+ * page: 1,
919
+ * limit: 20,
920
+ * isEmailVerified: true,
921
+ * hasSocialAuth: true,
922
+ * createdAt: { operator: 'gte', value: new Date('2024-01-01') },
923
+ * sortBy: 'createdAt',
924
+ * sortOrder: 'DESC'
925
+ * });
926
+ * ```
927
+ */
928
+ async getUsers(dto) {
929
+ // Ensure DTO is validated
930
+ dto = await (0, dto_validator_1.ensureValidatedDto)(get_users_dto_1.GetUsersDTO, dto);
931
+ this.logger?.debug?.(`Admin getUsers initiated with filters: ${JSON.stringify(dto)}`);
932
+ // ============================================================================
933
+ // Build Query with Filters
934
+ // ============================================================================
935
+ const qb = this.userRepository.createQueryBuilder('user');
936
+ // Apply partial match filters (email and phone) - case-insensitive
937
+ // Using LOWER() for cross-database compatibility (works on both MySQL and PostgreSQL)
938
+ if (dto.email) {
939
+ qb.andWhere('LOWER(user.email) LIKE LOWER(:email)', { email: `%${dto.email}%` });
940
+ }
941
+ if (dto.phone) {
942
+ qb.andWhere('LOWER(user.phone) LIKE LOWER(:phone)', { phone: `%${dto.phone}%` });
943
+ }
944
+ // Apply boolean filters
945
+ if (dto.isEmailVerified !== undefined) {
946
+ qb.andWhere('user.isEmailVerified = :isEmailVerified', { isEmailVerified: dto.isEmailVerified });
947
+ }
948
+ if (dto.isPhoneVerified !== undefined) {
949
+ qb.andWhere('user.isPhoneVerified = :isPhoneVerified', { isPhoneVerified: dto.isPhoneVerified });
950
+ }
951
+ if (dto.hasSocialAuth !== undefined) {
952
+ qb.andWhere('user.hasSocialAuth = :hasSocialAuth', { hasSocialAuth: dto.hasSocialAuth });
953
+ }
954
+ if (dto.isLocked !== undefined) {
955
+ qb.andWhere('user.isLocked = :isLocked', { isLocked: dto.isLocked });
956
+ }
957
+ if (dto.mfaEnabled !== undefined) {
958
+ qb.andWhere('user.mfaEnabled = :mfaEnabled', { mfaEnabled: dto.mfaEnabled });
959
+ }
960
+ // Apply date filters with operators
961
+ if (dto.createdAt) {
962
+ const { operator, value } = dto.createdAt;
963
+ if (operator === 'gt') {
964
+ qb.andWhere('user.createdAt > :createdAtValue', { createdAtValue: value });
965
+ }
966
+ else if (operator === 'gte') {
967
+ qb.andWhere('user.createdAt >= :createdAtValue', { createdAtValue: value });
968
+ }
969
+ else if (operator === 'lt') {
970
+ qb.andWhere('user.createdAt < :createdAtValue', { createdAtValue: value });
971
+ }
972
+ else if (operator === 'lte') {
973
+ qb.andWhere('user.createdAt <= :createdAtValue', { createdAtValue: value });
974
+ }
975
+ else if (operator === 'eq') {
976
+ qb.andWhere('user.createdAt = :createdAtValue', { createdAtValue: value });
977
+ }
978
+ }
979
+ if (dto.updatedAt) {
980
+ const { operator, value } = dto.updatedAt;
981
+ if (operator === 'gt') {
982
+ qb.andWhere('user.updatedAt > :updatedAtValue', { updatedAtValue: value });
983
+ }
984
+ else if (operator === 'gte') {
985
+ qb.andWhere('user.updatedAt >= :updatedAtValue', { updatedAtValue: value });
986
+ }
987
+ else if (operator === 'lt') {
988
+ qb.andWhere('user.updatedAt < :updatedAtValue', { updatedAtValue: value });
989
+ }
990
+ else if (operator === 'lte') {
991
+ qb.andWhere('user.updatedAt <= :updatedAtValue', { updatedAtValue: value });
992
+ }
993
+ else if (operator === 'eq') {
994
+ qb.andWhere('user.updatedAt = :updatedAtValue', { updatedAtValue: value });
995
+ }
996
+ }
997
+ // ============================================================================
998
+ // Apply Sorting
999
+ // ============================================================================
1000
+ const sortBy = dto.sortBy || 'createdAt';
1001
+ const sortOrder = dto.sortOrder || 'DESC';
1002
+ qb.orderBy(`user.${sortBy}`, sortOrder);
1003
+ // ============================================================================
1004
+ // Apply Pagination
1005
+ // ============================================================================
1006
+ const page = dto.page || 1;
1007
+ const limit = dto.limit || 10;
1008
+ qb.skip((page - 1) * limit).take(limit);
1009
+ // Execute query
1010
+ const [users, total] = await qb.getManyAndCount();
1011
+ this.logger?.debug?.(`Found ${users.length} users (total: ${total}) with filters`);
1012
+ // Sanitize user data
1013
+ const sanitizedUsers = users.map((user) => user_response_dto_1.UserResponseDto.fromEntity(user));
1014
+ return {
1015
+ users: sanitizedUsers,
1016
+ pagination: {
1017
+ page,
1018
+ limit,
1019
+ total,
1020
+ totalPages: Math.ceil(total / limit),
1021
+ },
1022
+ };
1023
+ }
1024
+ /**
1025
+ * Administrative permanent account locking
1026
+ *
1027
+ * Sets permanent lock (lockedUntil=NULL) and immediately revokes all active sessions.
1028
+ * Reuses existing rate-limit lock fields (isLocked, lockReason, lockedAt, lockedUntil).
1029
+ *
1030
+ * Permanent vs Temporary locks:
1031
+ * - Rate limiting: lockedUntil = future date (temporary auto-unlock)
1032
+ * - Admin disableUser: lockedUntil = NULL (permanent manual lock)
1033
+ *
1034
+ * Security:
1035
+ * - NO built-in authentication - endpoint MUST be protected by admin guards
1036
+ * - Revokes all sessions immediately (forced logout)
1037
+ * - Records ACCOUNT_DISABLED audit event with admin identifier
1038
+ *
1039
+ * @param dto - User sub and optional reason
1040
+ * @returns User object with updated lock status and revoked session count
1041
+ * @throws {NAuthException} USER_NOT_FOUND
1042
+ *
1043
+ * @example
1044
+ * ```typescript
1045
+ * const result = await authService.disableUser({
1046
+ * sub: 'user-uuid-123',
1047
+ * reason: 'Suspicious activity detected'
1048
+ * });
1049
+ * console.log(`Revoked ${result.revokedSessions} sessions`);
1050
+ * ```
1051
+ */
1052
+ async disableUser(dto) {
1053
+ // Ensure DTO is validated
1054
+ dto = await (0, dto_validator_1.ensureValidatedDto)(disable_user_dto_1.DisableUserDTO, dto);
1055
+ // Get client info for audit
1056
+ const clientInfo = this.clientInfoService.get();
1057
+ this.logger?.log?.(`Admin disableUser initiated for sub: ${dto.sub}`);
1058
+ // Find user by sub
1059
+ const user = await this.userRepository.findOne({ where: { sub: dto.sub } });
1060
+ if (!user) {
1061
+ this.logger?.warn?.(`User not found for disabling: ${dto.sub}`);
1062
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found');
1063
+ }
1064
+ this.logger?.debug?.(`Disabling user ${user.email} (id: ${user.id}, sub: ${dto.sub})`);
1065
+ // ============================================================================
1066
+ // Set Permanent Lock (lockedUntil = NULL)
1067
+ // ============================================================================
1068
+ // Use update() to ensure persistence and avoid entity state issues
1069
+ await this.userRepository.update({ id: user.id }, {
1070
+ isLocked: true,
1071
+ lockReason: dto.reason || 'Account disabled',
1072
+ lockedAt: new Date(),
1073
+ lockedUntil: null, // NULL = permanent lock (vs rate-limit's future date)
1074
+ });
1075
+ // Reload user to get updated entity with lock fields
1076
+ const updatedUser = (await this.userRepository.findOne({ where: { id: user.id } }));
1077
+ if (!updatedUser) {
1078
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found after update');
1079
+ }
1080
+ this.logger?.log?.(`User locked permanently: ${updatedUser.email} (sub: ${dto.sub})`);
1081
+ // ============================================================================
1082
+ // Revoke All Sessions (force logout)
1083
+ // ============================================================================
1084
+ let revokedCount = 0;
1085
+ try {
1086
+ revokedCount = await this.sessionService.revokeAllUserSessions(updatedUser.id, 'Account disabled');
1087
+ this.logger?.debug?.(`Revoked ${revokedCount} sessions for user ${dto.sub}`);
1088
+ }
1089
+ catch (sessionError) {
1090
+ // Non-blocking: Log but continue
1091
+ const errorMessage = sessionError instanceof Error ? sessionError.message : 'Unknown error';
1092
+ this.logger?.warn?.(`Failed to revoke sessions for user ${dto.sub}: ${errorMessage}`);
1093
+ }
1094
+ // ============================================================================
1095
+ // Record Admin Action (ACCOUNT_DISABLED)
1096
+ // ============================================================================
1097
+ if (!this.auditService) {
1098
+ this.logger?.warn?.(`Audit service not available - ACCOUNT_DISABLED event not recorded for user ${dto.sub}. Enable audit logs in config.auditLogs.enabled`);
1099
+ }
1100
+ else {
1101
+ try {
1102
+ // Get admin user ID from client info (the currently logged in user performing this action)
1103
+ // This is extracted from the JWT token by interceptors/handlers
1104
+ const adminUserId = clientInfo?.userId;
1105
+ // Set performedBy to the admin's user ID (who locked the account)
1106
+ // This identifies which admin user performed the action in the audit trail
1107
+ const performedBy = adminUserId ? String(adminUserId) : clientInfo.ipAddress || 'system';
1108
+ if (adminUserId) {
1109
+ this.logger?.debug?.(`Admin user ID ${adminUserId} (currently logged in) is disabling account for user ${dto.sub}`);
1110
+ }
1111
+ else {
1112
+ this.logger?.warn?.(`No admin user ID in clientInfo - performedBy will be set to IP address or 'system' for user ${dto.sub}`);
1113
+ }
1114
+ const auditResult = await this.auditService.recordEvent({
1115
+ userId: updatedUser.id, // The user whose account is being disabled
1116
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_DISABLED,
1117
+ eventStatus: 'INFO',
1118
+ authMethod: 'admin',
1119
+ performedBy, // The admin user ID (currently logged in user) who performed this action
1120
+ reason: updatedUser.lockReason || 'Account disabled',
1121
+ description: `Account disabled by administrator. User: ${updatedUser.email} (sub: ${dto.sub}). ${revokedCount} session(s) revoked.`,
1122
+ metadata: {
1123
+ email: updatedUser.email,
1124
+ userSub: dto.sub,
1125
+ reason: updatedUser.lockReason,
1126
+ adminIdentifier: clientInfo.ipAddress || 'unknown',
1127
+ adminUserId: adminUserId || null,
1128
+ revokedSessions: revokedCount,
1129
+ lockedAt: updatedUser.lockedAt,
1130
+ lockedUntil: updatedUser.lockedUntil,
1131
+ },
1132
+ });
1133
+ if (auditResult) {
1134
+ this.logger?.debug?.(`ACCOUNT_DISABLED audit event recorded successfully for user ${dto.sub}`);
1135
+ }
1136
+ else {
1137
+ this.logger?.warn?.(`ACCOUNT_DISABLED audit event returned null for user ${dto.sub}`);
1138
+ }
1139
+ }
1140
+ catch (auditError) {
1141
+ // Non-blocking: Log but continue
1142
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
1143
+ const errorStack = auditError instanceof Error ? auditError.stack : undefined;
1144
+ this.logger?.error?.(`Failed to record ACCOUNT_DISABLED audit event: ${errorMessage}`, {
1145
+ error: auditError,
1146
+ errorStack,
1147
+ userId: updatedUser.id,
1148
+ userSub: dto.sub,
1149
+ });
1150
+ }
1151
+ }
1152
+ // Return sanitized user and revoked session count
1153
+ const userDto = user_response_dto_1.UserResponseDto.fromEntity(updatedUser);
1154
+ return {
1155
+ success: true,
1156
+ user: userDto,
1157
+ revokedSessions: revokedCount,
1158
+ };
1159
+ }
1160
+ /**
1161
+ * Enable (unlock) user account
1162
+ *
1163
+ * Unlocks a previously locked user account by clearing all lock fields.
1164
+ * This reverses the effect of disableUser() or rate-limit lockouts.
1165
+ *
1166
+ * Security:
1167
+ * - NO built-in authentication - endpoint MUST be protected by admin guards
1168
+ * - Clears lock fields (isLocked, lockReason, lockedAt, lockedUntil)
1169
+ * - Resets failed login attempts counter
1170
+ * - Records ACCOUNT_ENABLED audit event with admin identifier
1171
+ *
1172
+ * @param dto - User sub to enable
1173
+ * @returns User object with updated lock status
1174
+ * @throws {NAuthException} USER_NOT_FOUND
1175
+ *
1176
+ * @example
1177
+ * ```typescript
1178
+ * const result = await authService.enableUser({
1179
+ * sub: 'user-uuid-123'
1180
+ * });
1181
+ * console.log(`User unlocked: ${result.user.email}`);
1182
+ * ```
1183
+ */
1184
+ async enableUser(dto) {
1185
+ // Ensure DTO is validated
1186
+ dto = await (0, dto_validator_1.ensureValidatedDto)(enable_user_dto_1.EnableUserDTO, dto);
1187
+ // Get client info for audit
1188
+ const clientInfo = this.clientInfoService.get();
1189
+ this.logger?.log?.(`Admin enableUser initiated for sub: ${dto.sub}`);
1190
+ // Find user by sub
1191
+ const user = await this.userRepository.findOne({ where: { sub: dto.sub } });
1192
+ if (!user) {
1193
+ this.logger?.warn?.(`User not found for enabling: ${dto.sub}`);
1194
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found');
1195
+ }
1196
+ this.logger?.debug?.(`Enabling user ${user.email} (id: ${user.id}, sub: ${dto.sub})`);
1197
+ // ============================================================================
1198
+ // Clear Lock Fields (unlock account)
1199
+ // ============================================================================
1200
+ await this.userRepository.update({ id: user.id }, {
1201
+ isLocked: false,
1202
+ lockReason: null,
1203
+ lockedAt: null,
1204
+ lockedUntil: null,
1205
+ failedLoginAttempts: 0, // Reset failed attempts counter
1206
+ });
1207
+ // Reload user to get updated entity
1208
+ const updatedUser = (await this.userRepository.findOne({ where: { id: user.id } }));
1209
+ if (!updatedUser) {
1210
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USER_NOT_FOUND, 'User not found after update');
1211
+ }
1212
+ this.logger?.log?.(`User unlocked: ${updatedUser.email} (sub: ${dto.sub})`);
1213
+ // ============================================================================
1214
+ // Record Admin Action (ACCOUNT_ENABLED)
1215
+ // ============================================================================
1216
+ if (!this.auditService) {
1217
+ this.logger?.warn?.(`Audit service not available - ACCOUNT_ENABLED event not recorded for user ${dto.sub}. Enable audit logs in config.auditLogs.enabled`);
1218
+ }
1219
+ else {
1220
+ try {
1221
+ // Get admin user ID from client info (the currently logged in user performing this action)
1222
+ const adminUserId = clientInfo?.userId;
1223
+ // Set performedBy to the admin's user ID (who unlocked the account)
1224
+ const performedBy = adminUserId ? String(adminUserId) : clientInfo.ipAddress || 'system';
1225
+ if (adminUserId) {
1226
+ this.logger?.debug?.(`Admin user ID ${adminUserId} (currently logged in) is enabling account for user ${dto.sub}`);
1227
+ }
1228
+ else {
1229
+ this.logger?.warn?.(`No admin user ID in clientInfo - performedBy will be set to IP address or 'system' for user ${dto.sub}`);
1230
+ }
1231
+ const auditResult = await this.auditService.recordEvent({
1232
+ userId: updatedUser.id,
1233
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_ENABLED,
1234
+ eventStatus: 'INFO',
1235
+ authMethod: 'admin',
1236
+ performedBy,
1237
+ reason: 'admin_unlock',
1238
+ description: 'Account unlocked by administrator',
1239
+ metadata: {
1240
+ userSub: dto.sub,
1241
+ adminIdentifier: clientInfo.ipAddress || 'unknown',
1242
+ adminUserId: adminUserId || null,
1243
+ previousLockReason: user.lockReason,
1244
+ previousLockedAt: user.lockedAt,
1245
+ previousLockedUntil: user.lockedUntil,
1246
+ },
1247
+ });
1248
+ if (auditResult) {
1249
+ this.logger?.debug?.(`ACCOUNT_ENABLED audit event recorded successfully for user ${dto.sub}`);
1250
+ }
1251
+ else {
1252
+ this.logger?.warn?.(`ACCOUNT_ENABLED audit event returned null for user ${dto.sub}`);
1253
+ }
1254
+ }
1255
+ catch (auditError) {
1256
+ // Non-blocking: Log but continue
1257
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
1258
+ const errorStack = auditError instanceof Error ? auditError.stack : undefined;
1259
+ this.logger?.error?.(`Failed to record ACCOUNT_ENABLED audit event: ${errorMessage}`, {
1260
+ error: auditError,
1261
+ errorStack,
1262
+ userId: updatedUser.id,
1263
+ userSub: dto.sub,
1264
+ });
1265
+ }
1266
+ }
1267
+ // Return sanitized user
1268
+ const userDto = user_response_dto_1.UserResponseDto.fromEntity(updatedUser);
1269
+ return {
1270
+ success: true,
1271
+ user: userDto,
1272
+ };
1273
+ }
1274
+ // ============================================================================
507
1275
  // User Login
508
1276
  // ============================================================================
509
1277
  /**
@@ -665,6 +1433,89 @@ class AuthService {
665
1433
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INVALID_CREDENTIALS, 'Invalid credentials');
666
1434
  }
667
1435
  // ============================================================================
1436
+ // Account Lock Check (Admin Disabled / Rate Limit Lockout)
1437
+ // ============================================================================
1438
+ // Check if account is permanently locked (lockedUntil = NULL) or temporarily locked (lockedUntil > now)
1439
+ if (user.isLocked) {
1440
+ const now = new Date();
1441
+ const isPermanentlyLocked = user.lockedUntil === null;
1442
+ const isTemporarilyLocked = user.lockedUntil && new Date(user.lockedUntil) > now;
1443
+ if (isPermanentlyLocked || isTemporarilyLocked) {
1444
+ const lockReason = user.lockReason || 'Account is locked';
1445
+ this.logger?.warn?.(`Login blocked - account locked for user: ${user.email} (sub: ${user.sub}). Reason: ${lockReason}`);
1446
+ // Record blocked login attempt
1447
+ await this.recordLoginAttempt(dto.identifier, false, 'account_locked');
1448
+ // ============================================================================
1449
+ // Audit: Record blocked login (account locked)
1450
+ // ============================================================================
1451
+ if (fireAndForget) {
1452
+ this.auditService
1453
+ ?.recordEvent({
1454
+ userId: user.id,
1455
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.LOGIN_BLOCKED,
1456
+ eventStatus: 'FAILURE',
1457
+ authMethod: 'password',
1458
+ reason: 'account_locked',
1459
+ description: `Login blocked - account locked: ${lockReason}`,
1460
+ metadata: {
1461
+ lockReason: user.lockReason,
1462
+ lockedAt: user.lockedAt,
1463
+ lockedUntil: user.lockedUntil,
1464
+ isPermanent: isPermanentlyLocked,
1465
+ },
1466
+ })
1467
+ .catch((err) => {
1468
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
1469
+ this.logger?.error?.(`Failed to record LOGIN_BLOCKED audit event (fire-and-forget): ${errorMessage}`, {
1470
+ error: err,
1471
+ userId: user.id,
1472
+ userSub: user.sub,
1473
+ });
1474
+ });
1475
+ }
1476
+ else {
1477
+ try {
1478
+ await this.auditService?.recordEvent({
1479
+ userId: user.id,
1480
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.LOGIN_BLOCKED,
1481
+ eventStatus: 'FAILURE',
1482
+ authMethod: 'password',
1483
+ reason: 'account_locked',
1484
+ description: `Login blocked - account locked: ${lockReason}`,
1485
+ metadata: {
1486
+ lockReason: user.lockReason,
1487
+ lockedAt: user.lockedAt,
1488
+ lockedUntil: user.lockedUntil,
1489
+ isPermanent: isPermanentlyLocked,
1490
+ },
1491
+ });
1492
+ }
1493
+ catch (auditError) {
1494
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
1495
+ this.logger?.error?.(`Failed to record LOGIN_BLOCKED audit event (account locked): ${errorMessage}`, {
1496
+ error: auditError,
1497
+ userId: user.id,
1498
+ });
1499
+ }
1500
+ }
1501
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.ACCOUNT_LOCKED, lockReason, {
1502
+ lockReason: user.lockReason,
1503
+ lockedAt: user.lockedAt,
1504
+ lockedUntil: user.lockedUntil,
1505
+ isPermanent: isPermanentlyLocked,
1506
+ });
1507
+ }
1508
+ else {
1509
+ // Account was temporarily locked but lock has expired - unlock it
1510
+ this.logger?.debug?.(`Account lock expired for user: ${user.email} (sub: ${user.sub}), unlocking account`);
1511
+ user.isLocked = false;
1512
+ user.lockReason = null;
1513
+ user.lockedAt = null;
1514
+ user.lockedUntil = null;
1515
+ await this.userRepository.save(user);
1516
+ }
1517
+ }
1518
+ // ============================================================================
668
1519
  // Password Expiry Check
669
1520
  // ============================================================================
670
1521
  const expiryDays = this.config.password?.expiryDays;
@@ -1910,7 +2761,11 @@ class AuthService {
1910
2761
  switch (challengeSession.challengeName) {
1911
2762
  case auth_challenge_dto_1.AuthChallenge.VERIFY_EMAIL: {
1912
2763
  // Resend email verification
1913
- const resendDto = Object.assign(new verify_email_dto_1.ResendVerificationEmailDTO(), { sub: user.sub });
2764
+ // Pass challengeSessionId to ensure new token is linked to this challenge session
2765
+ const resendDto = Object.assign(new verify_email_dto_1.ResendVerificationEmailDTO(), {
2766
+ sub: user.sub,
2767
+ challengeSessionId: challengeSession.id,
2768
+ });
1914
2769
  await this.emailVerificationService.resendVerificationEmail(resendDto);
1915
2770
  const maskedEmail = this.maskEmail(user.email);
1916
2771
  this.logger?.debug?.(`Email verification code resent: user=${user.sub}, email=${maskedEmail}`);
@@ -3220,6 +4075,11 @@ class AuthService {
3220
4075
  'user.isEmailVerified',
3221
4076
  'user.isPhoneVerified',
3222
4077
  'user.mfaExempt', // Required for MFA exemption check in challenge flow
4078
+ // Lock fields - required for account lock check in login flow
4079
+ 'user.isLocked',
4080
+ 'user.lockReason',
4081
+ 'user.lockedAt',
4082
+ 'user.lockedUntil',
3223
4083
  // The following are used for messaging/challenge determination when needed
3224
4084
  'user.socialProviders',
3225
4085
  'user.backupCodes',