@nauth-toolkit/core 0.1.0 → 0.1.5

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 (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +9 -0
  3. package/package.json +8 -3
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,238 +0,0 @@
1
- import { SocialAuthService } from './social-auth.service';
2
- import { ISocialAuthProviderService } from '../interfaces/social-auth-provider.interface';
3
- import { NAuthException } from '../exceptions/nauth.exception';
4
- import { AuthErrorCode } from '../enums/error-codes.enum';
5
-
6
- /**
7
- * Social Auth Service Unit Tests
8
- *
9
- * Tests social authentication provider registry functionality.
10
- * Covers provider registration, lookup, and listing.
11
- *
12
- * Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
13
- */
14
- describe('SocialAuthService', () => {
15
- let service: SocialAuthService;
16
- let mockProvider1: jest.Mocked<ISocialAuthProviderService>;
17
- let mockProvider2: jest.Mocked<ISocialAuthProviderService>;
18
-
19
- beforeEach(() => {
20
- // Create mock providers
21
- mockProvider1 = {
22
- providerName: 'google',
23
- getAuthUrl: jest.fn(),
24
- handleCallback: jest.fn(),
25
- verifyToken: jest.fn(),
26
- linkAccount: jest.fn(),
27
- getUserProfileFromCallback: jest.fn(),
28
- } as any;
29
-
30
- mockProvider2 = {
31
- providerName: 'apple',
32
- getAuthUrl: jest.fn(),
33
- handleCallback: jest.fn(),
34
- verifyToken: jest.fn(),
35
- linkAccount: jest.fn(),
36
- getUserProfileFromCallback: jest.fn(),
37
- } as any;
38
-
39
- // Instantiate service directly
40
- service = new SocialAuthService();
41
- });
42
-
43
- afterEach(() => {
44
- jest.clearAllMocks();
45
- });
46
-
47
- // ============================================================================
48
- // Service Initialization
49
- // ============================================================================
50
-
51
- it('should be defined', () => {
52
- expect(service).toBeDefined();
53
- });
54
-
55
- // ============================================================================
56
- // registerProvider() Method
57
- // ============================================================================
58
-
59
- describe('registerProvider', () => {
60
- it('should register provider successfully', () => {
61
- service.registerProvider(mockProvider1);
62
-
63
- expect(service.hasProvider('google')).toBe(true);
64
- });
65
-
66
- it('should throw error when provider already registered', () => {
67
- service.registerProvider(mockProvider1);
68
-
69
- expect(() => service.registerProvider(mockProvider1)).toThrow(NAuthException);
70
- expect(() => service.registerProvider(mockProvider1)).toThrow('already registered');
71
- });
72
-
73
- it('should allow multiple different providers', () => {
74
- service.registerProvider(mockProvider1);
75
- service.registerProvider(mockProvider2);
76
-
77
- expect(service.hasProvider('google')).toBe(true);
78
- expect(service.hasProvider('apple')).toBe(true);
79
- });
80
-
81
- it('should register provider with correct name', () => {
82
- service.registerProvider(mockProvider1);
83
-
84
- const provider = service.getProvider('google');
85
- expect(provider).toBe(mockProvider1);
86
- expect(provider.providerName).toBe('google');
87
- });
88
- });
89
-
90
- // ============================================================================
91
- // getProvider() Method
92
- // ============================================================================
93
-
94
- describe('getProvider', () => {
95
- it('should return registered provider', () => {
96
- service.registerProvider(mockProvider1);
97
-
98
- const provider = service.getProvider('google');
99
-
100
- expect(provider).toBe(mockProvider1);
101
- });
102
-
103
- it('should throw error when provider not registered', () => {
104
- expect(() => service.getProvider('google')).toThrow(NAuthException);
105
- expect(() => service.getProvider('google')).toThrow('not registered');
106
- });
107
-
108
- it('should throw error with helpful message suggesting module import', () => {
109
- try {
110
- service.getProvider('facebook');
111
- fail('Should have thrown NAuthException');
112
- } catch (error: any) {
113
- expect(error).toBeInstanceOf(NAuthException);
114
- expect(error.message).toContain('Import the provider module');
115
- }
116
- });
117
-
118
- it('should use correct error code when provider not found', () => {
119
- try {
120
- service.getProvider('google');
121
- } catch (error) {
122
- expect(error).toBeInstanceOf(NAuthException);
123
- expect((error as NAuthException).code).toBe(AuthErrorCode.SOCIAL_CONFIG_MISSING);
124
- }
125
- });
126
- });
127
-
128
- // ============================================================================
129
- // hasProvider() Method
130
- // ============================================================================
131
-
132
- describe('hasProvider', () => {
133
- it('should return true for registered provider', () => {
134
- service.registerProvider(mockProvider1);
135
-
136
- expect(service.hasProvider('google')).toBe(true);
137
- });
138
-
139
- it('should return false for unregistered provider', () => {
140
- expect(service.hasProvider('google')).toBe(false);
141
- });
142
-
143
- it('should return false for provider that was never registered', () => {
144
- service.registerProvider(mockProvider1);
145
-
146
- expect(service.hasProvider('facebook')).toBe(false);
147
- });
148
- });
149
-
150
- // ============================================================================
151
- // listProviders() Method
152
- // ============================================================================
153
-
154
- describe('listProviders', () => {
155
- it('should return empty array when no providers registered', () => {
156
- expect(service.listProviders()).toEqual([]);
157
- });
158
-
159
- it('should return all registered provider names', () => {
160
- service.registerProvider(mockProvider1);
161
- service.registerProvider(mockProvider2);
162
-
163
- const providers = service.listProviders();
164
-
165
- expect(providers).toContain('google');
166
- expect(providers).toContain('apple');
167
- expect(providers.length).toBe(2);
168
- });
169
-
170
- it('should return provider names in registration order', () => {
171
- service.registerProvider(mockProvider1);
172
- service.registerProvider(mockProvider2);
173
-
174
- const providers = service.listProviders();
175
-
176
- expect(providers[0]).toBe('google');
177
- expect(providers[1]).toBe('apple');
178
- });
179
-
180
- it('should return updated list after new provider registered', () => {
181
- expect(service.listProviders()).toEqual([]);
182
-
183
- service.registerProvider(mockProvider1);
184
- expect(service.listProviders()).toEqual(['google']);
185
-
186
- service.registerProvider(mockProvider2);
187
- expect(service.listProviders()).toEqual(['google', 'apple']);
188
- });
189
- });
190
-
191
- // ============================================================================
192
- // Integration Tests
193
- // ============================================================================
194
-
195
- describe('Integration', () => {
196
- it('should allow full provider lifecycle', () => {
197
- // Register
198
- service.registerProvider(mockProvider1);
199
- expect(service.hasProvider('google')).toBe(true);
200
-
201
- // Get
202
- const provider = service.getProvider('google');
203
- expect(provider).toBe(mockProvider1);
204
-
205
- // List
206
- const providers = service.listProviders();
207
- expect(providers).toContain('google');
208
- });
209
-
210
- it('should handle multiple providers independently', () => {
211
- service.registerProvider(mockProvider1);
212
- service.registerProvider(mockProvider2);
213
-
214
- const googleProvider = service.getProvider('google');
215
- const appleProvider = service.getProvider('apple');
216
-
217
- expect(googleProvider).toBe(mockProvider1);
218
- expect(appleProvider).toBe(mockProvider2);
219
- expect(googleProvider).not.toBe(appleProvider);
220
- });
221
-
222
- it('should maintain provider registry across operations', () => {
223
- service.registerProvider(mockProvider1);
224
- service.registerProvider(mockProvider2);
225
-
226
- // Verify both still registered
227
- expect(service.hasProvider('google')).toBe(true);
228
- expect(service.hasProvider('apple')).toBe(true);
229
-
230
- // Get both
231
- const google = service.getProvider('google');
232
- const apple = service.getProvider('apple');
233
-
234
- expect(google).toBe(mockProvider1);
235
- expect(apple).toBe(mockProvider2);
236
- });
237
- });
238
- });
@@ -1,436 +0,0 @@
1
- import { Repository } from 'typeorm';
2
- import { IUser, ISocialAccount } from '../interfaces/entities.interface';
3
- import { BaseUser, BaseSocialAccount } from '../entities';
4
- import { AuthService } from './auth.service';
5
- import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service';
6
- import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
7
- import { NAuthException } from '../exceptions/nauth.exception';
8
- import { AuthErrorCode } from '../enums/error-codes.enum';
9
- import { NAuthLogger } from '../utils/nauth-logger';
10
- import { ChangePasswordRequestDTO } from '../dto/change-password-request.dto';
11
- import { SocialProviderRegistry } from './social-provider-registry.service';
12
- import { AuthResponseDTO } from '../dto/auth-response.dto';
13
- import {
14
- GetSocialAuthUrlDTO,
15
- GetSocialAuthUrlResponseDTO,
16
- HandleSocialCallbackDTO,
17
- LinkSocialAccountDTO,
18
- LinkSocialAccountResponseDTO,
19
- GetLinkedAccountsDTO,
20
- GetLinkedAccountsResponseDTO,
21
- UnlinkSocialAccountDTO,
22
- UnlinkSocialAccountResponseDTO,
23
- CanSetPasswordDTO,
24
- CanSetPasswordResponseDTO,
25
- SetPasswordForSocialUserDTO,
26
- SetPasswordForSocialUserResponseDTO,
27
- } from '../dto/social-auth.dto';
28
-
29
- /**
30
- * Social Auth Service
31
- *
32
- * Complete API for social authentication (OAuth) and account management.
33
- * This service provides:
34
- * - OAuth authentication flows (login/signup via social providers)
35
- * - Social account linking/unlinking
36
- * - Account management for social users
37
- * - Password management for social-only users
38
- *
39
- * **Optional Feature:** Only available when social auth provider modules are imported.
40
- *
41
- * **Usage:**
42
- * ```typescript
43
- * // NestJS
44
- * imports: [
45
- * AuthModule.forRoot(config),
46
- * GoogleSocialAuthModule, // Enables Google OAuth
47
- * AppleSocialAuthModule, // Enables Apple Sign In
48
- * ]
49
- *
50
- * // Then inject and use
51
- * constructor(private socialAuthService: SocialAuthService) {}
52
- *
53
- * const { url } = await this.socialAuthService.getSocialAuthUrl({ provider: 'google' });
54
- * const result = await this.socialAuthService.handleSocialCallback({ provider: 'google', code, state });
55
- * ```
56
- */
57
- export class SocialAuthService {
58
- constructor(
59
- private readonly providerRegistry: SocialProviderRegistry,
60
- private readonly userRepository: Repository<BaseUser>,
61
- private readonly socialAccountRepository: Repository<BaseSocialAccount>,
62
- private readonly authService: AuthService,
63
- private readonly logger: NAuthLogger,
64
- private readonly auditService?: AuthAuditService, // Optional - audit trail service (enabled via config.auditLogs.enabled)
65
- ) {}
66
-
67
- // ============================================================================
68
- // Social Authentication Methods
69
- // ============================================================================
70
-
71
- /**
72
- * Get social authentication URL
73
- *
74
- * Generates OAuth authorization URL for the specified provider.
75
- * This is the first step in the OAuth flow - redirect user to this URL.
76
- *
77
- * @param dto - Request DTO containing provider and optional state
78
- * @returns Response DTO with OAuth authorization URL
79
- * @throws {NAuthException} SOCIAL_CONFIG_MISSING if provider not registered or configured
80
- *
81
- * @example
82
- * ```typescript
83
- * const dto = { provider: 'google', state: 'csrf-token-123' };
84
- * const { url } = await socialAuthService.getSocialAuthUrl(dto);
85
- * // Redirect user to url
86
- * res.redirect(url);
87
- * ```
88
- */
89
- async getSocialAuthUrl(dto: GetSocialAuthUrlDTO): Promise<GetSocialAuthUrlResponseDTO> {
90
- const { provider, state } = dto;
91
- const providerInstance = this.providerRegistry.getProvider(provider);
92
- const url = await providerInstance.getAuthUrl(state);
93
- return { url };
94
- }
95
-
96
- /**
97
- * Handle social authentication callback
98
- *
99
- * Processes OAuth callback and authenticates user (login or signup).
100
- * This is called after the user is redirected back from the OAuth provider.
101
- *
102
- * @param dto - Request DTO containing provider, code, and state
103
- * @returns Auth response (tokens or challenge if MFA/verification required)
104
- * @throws {NAuthException} Various auth errors (SOCIAL_AUTH_FAILED, etc.)
105
- *
106
- * @example
107
- * ```typescript
108
- * const dto = {
109
- * provider: 'google',
110
- * code: req.query.code,
111
- * state: req.query.state
112
- * };
113
- * const result = await socialAuthService.handleSocialCallback(dto);
114
- * // Returns tokens or challenge
115
- * ```
116
- */
117
- async handleSocialCallback(dto: HandleSocialCallbackDTO): Promise<AuthResponseDTO> {
118
- const { provider, code, state } = dto;
119
- const providerInstance = this.providerRegistry.getProvider(provider);
120
- return await providerInstance.handleCallback(code, state);
121
- }
122
-
123
- /**
124
- * Link social account to existing authenticated user
125
- *
126
- * Connects a social provider to an already logged-in user's account.
127
- * User must be authenticated before calling this method.
128
- *
129
- * @param dto - Request DTO containing userId, provider, code, and state
130
- * @returns Response DTO with success message and provider name
131
- * @throws {NAuthException} SOCIAL_ALREADY_LINKED, NOT_FOUND, etc.
132
- *
133
- * @example
134
- * ```typescript
135
- * const dto = {
136
- * userId: user.sub,
137
- * provider: 'apple',
138
- * code: req.query.code,
139
- * state: req.query.state
140
- * };
141
- * const result = await socialAuthService.linkSocialAccount(dto);
142
- * ```
143
- */
144
- async linkSocialAccount(dto: LinkSocialAccountDTO): Promise<LinkSocialAccountResponseDTO> {
145
- const { userId, provider, code, state } = dto;
146
- const providerInstance = this.providerRegistry.getProvider(provider);
147
- const result = await providerInstance.linkAccount(userId, code, state);
148
- return { ...result, provider };
149
- }
150
-
151
- /**
152
- * List available social auth providers
153
- *
154
- * Returns names of all registered and enabled social auth providers.
155
- * Useful for displaying available login options in the UI.
156
- *
157
- * @returns Array of provider names (e.g., ['google', 'apple', 'facebook'])
158
- *
159
- * @example
160
- * ```typescript
161
- * const providers = socialAuthService.listAvailableProviders();
162
- * // Display social login buttons based on available providers
163
- * ```
164
- */
165
- listAvailableProviders(): string[] {
166
- return this.providerRegistry.listProviders();
167
- }
168
-
169
- // ============================================================================
170
- // Social Account Management Methods
171
- // ============================================================================
172
-
173
- /**
174
- * Get linked social accounts for a user
175
- *
176
- * @param dto - Request DTO containing userId
177
- * @returns Response DTO with array of linked social accounts
178
- * @throws {NAuthException} NOT_FOUND when user is not found
179
- *
180
- * @example
181
- * ```typescript
182
- * const dto = { userId: 'user-uuid' };
183
- * const accounts = await socialAuthService.getLinkedAccounts(dto);
184
- * console.log(accounts.accounts); // [{ provider: 'google', ... }]
185
- * ```
186
- */
187
- async getLinkedAccounts(dto: GetLinkedAccountsDTO): Promise<GetLinkedAccountsResponseDTO> {
188
- const { userId } = dto;
189
- const user = (await this.userRepository.findOne({ where: { sub: userId } })) as IUser | null;
190
- if (!user) {
191
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
192
- }
193
-
194
- const socialAccounts = (await this.socialAccountRepository.find({
195
- where: { userId: user.id },
196
- order: { linkedAt: 'DESC' },
197
- })) as ISocialAccount[];
198
-
199
- return {
200
- accounts: socialAccounts.map((account) => ({
201
- provider: account.provider,
202
- providerEmail: account.providerEmail || undefined,
203
- linkedAt: account.linkedAt,
204
- lastUsedAt: account.lastUsedAt || undefined,
205
- })),
206
- };
207
- }
208
-
209
- /**
210
- * Unlink social account from user
211
- *
212
- * @param dto - Request DTO containing userId and provider
213
- * @returns Response DTO with success message
214
- * @throws {NAuthException} NOT_FOUND when user or account is not found
215
- *
216
- * @example
217
- * ```typescript
218
- * const dto = { userId: 'user-uuid', provider: 'google' };
219
- * await socialAuthService.unlinkSocialAccount(dto);
220
- * ```
221
- */
222
- async unlinkSocialAccount(dto: UnlinkSocialAccountDTO): Promise<UnlinkSocialAccountResponseDTO> {
223
- const { userId, provider } = dto;
224
- const user = (await this.userRepository.findOne({ where: { sub: userId } })) as IUser | null;
225
- if (!user) {
226
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
227
- }
228
-
229
- const socialAccount = (await this.socialAccountRepository.findOne({
230
- where: { userId: user.id, provider },
231
- })) as ISocialAccount | null;
232
-
233
- if (!socialAccount) {
234
- throw new NAuthException(
235
- AuthErrorCode.SOCIAL_ACCOUNT_NOT_FOUND,
236
- `${provider} account is not linked to this user`,
237
- );
238
- }
239
-
240
- // Delete social account
241
- await this.socialAccountRepository.remove(socialAccount);
242
-
243
- // Update user's social auth flags
244
- await this.updateUserSocialFlags(user.id as number);
245
-
246
- // ============================================================================
247
- // Audit: Record social account unlink
248
- // ============================================================================
249
- try {
250
- await this.auditService?.recordEvent({
251
- userId: user.id,
252
- eventType: AuthAuditEventType.SOCIAL_ACCOUNT_UNLINKED,
253
- eventStatus: 'INFO',
254
- authMethod: provider,
255
- // Client info automatically included from context
256
- metadata: {
257
- provider,
258
- providerEmail: socialAccount.providerEmail || null,
259
- },
260
- });
261
- } catch (auditError) {
262
- // Non-blocking: Log but continue
263
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
264
- this.logger?.error?.(`Failed to record SOCIAL_ACCOUNT_UNLINKED audit event: ${errorMessage}`, {
265
- error: auditError,
266
- userId: user.id,
267
- provider,
268
- });
269
- }
270
-
271
- return { message: `${provider} account unlinked successfully` };
272
- }
273
-
274
- /**
275
- * Check if user can set a password
276
- * Users with social-only accounts can set passwords
277
- *
278
- * @param dto - Request DTO containing userId
279
- * @returns Response DTO indicating whether user can set password
280
- *
281
- * @example
282
- * ```typescript
283
- * const dto = { userId: 'user-uuid' };
284
- * const result = await socialAuthService.canSetPassword(dto);
285
- * if (result.canSetPassword) {
286
- * // Allow user to set password
287
- * }
288
- * ```
289
- */
290
- async canSetPassword(dto: CanSetPasswordDTO): Promise<CanSetPasswordResponseDTO> {
291
- const { userId } = dto;
292
- const user = (await this.userRepository.findOne({ where: { sub: userId } })) as IUser | null;
293
- if (!user) {
294
- return { canSetPassword: false };
295
- }
296
-
297
- // User can set password if they don't have one (social-only account)
298
- return { canSetPassword: !user.passwordHash };
299
- }
300
-
301
- /**
302
- * Set password for social-only user
303
- *
304
- * @param dto - Request DTO containing userId and password
305
- * @returns Response DTO with success message
306
- * @throws {NAuthException} NOT_FOUND when user is not found
307
- * @throws {NAuthException} VALIDATION_FAILED when user already has a password
308
- *
309
- * @example
310
- * ```typescript
311
- * const dto = { userId: 'user-uuid', password: 'newpassword' };
312
- * await socialAuthService.setPasswordForSocialUser(dto);
313
- * ```
314
- */
315
- async setPasswordForSocialUser(dto: SetPasswordForSocialUserDTO): Promise<SetPasswordForSocialUserResponseDTO> {
316
- const { userId, password } = dto;
317
- const user = await this.userRepository.findOne({ where: { sub: userId } });
318
- if (!user) {
319
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
320
- }
321
-
322
- if (user.passwordHash) {
323
- throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'User already has a password', {
324
- field: 'password',
325
- });
326
- }
327
-
328
- // Use AuthService to set password (includes validation and hashing)
329
- // For social-only users, we bypass old password validation since they don't have one
330
- // Note: This requires type casting as ChangePasswordRequestDTO requires oldPassword, but
331
- // the auth service will handle the case where user has no passwordHash
332
- const changePasswordDto = new ChangePasswordRequestDTO();
333
- changePasswordDto.sub = userId; // userId is the sub (external UUID) in this context
334
- changePasswordDto.oldPassword = ''; // Social-only users don't have a password
335
- changePasswordDto.newPassword = password;
336
- await this.authService.changePassword(changePasswordDto);
337
-
338
- return { message: 'Password set successfully' };
339
- }
340
-
341
- /**
342
- * Find social account by provider and provider ID
343
- *
344
- * @param provider - Provider name (e.g., 'google', 'apple')
345
- * @param providerId - Provider user ID
346
- * @returns Social account with user relation, or null
347
- * @internal - For use by BaseSocialAuthProviderService
348
- */
349
- async findSocialAccountByProvider(provider: string, providerId: string): Promise<ISocialAccount | null> {
350
- return (await this.socialAccountRepository.findOne({
351
- where: { provider, providerId },
352
- relations: ['user'],
353
- })) as ISocialAccount | null;
354
- }
355
-
356
- /**
357
- * Find social account by user ID and provider
358
- *
359
- * @param userId - User ID (internal)
360
- * @param provider - Provider name
361
- * @returns Social account or null
362
- * @internal - For use by BaseSocialAuthProviderService
363
- */
364
- async findSocialAccountByUser(userId: number, provider: string): Promise<ISocialAccount | null> {
365
- return (await this.socialAccountRepository.findOne({
366
- where: { userId, provider },
367
- })) as ISocialAccount | null;
368
- }
369
-
370
- /**
371
- * Create or update social account
372
- *
373
- * @param userId - User ID (internal)
374
- * @param provider - Provider name
375
- * @param providerId - Provider user ID
376
- * @param providerEmail - Provider email
377
- * @param metadata - Optional raw profile data
378
- * @internal - For use by BaseSocialAuthProviderService
379
- */
380
- async createOrUpdateSocialAccount(
381
- userId: number,
382
- provider: string,
383
- providerId: string,
384
- providerEmail?: string | null,
385
- metadata?: any,
386
- ): Promise<void> {
387
- const existingAccount = await this.findSocialAccountByUser(userId, provider);
388
-
389
- if (existingAccount) {
390
- // Update existing account
391
- existingAccount.providerEmail = providerEmail || null;
392
- existingAccount.lastUsedAt = new Date();
393
- existingAccount.metadata = metadata || null;
394
- await this.socialAccountRepository.save(existingAccount);
395
- } else {
396
- // Create new account
397
- const socialAccount = this.socialAccountRepository.create({
398
- userId,
399
- provider,
400
- providerId,
401
- providerEmail: providerEmail || null,
402
- linkedAt: new Date(),
403
- lastUsedAt: new Date(),
404
- metadata: metadata || null,
405
- });
406
-
407
- await this.socialAccountRepository.save(socialAccount);
408
- }
409
-
410
- // Update user's social auth flags
411
- await this.updateUserSocialFlags(userId);
412
- }
413
-
414
- /**
415
- * Update user's social authentication flags
416
- *
417
- * @param userId - User ID (internal)
418
- * @internal - For use by BaseSocialAuthProviderService
419
- */
420
- async updateUserSocialFlags(userId: number): Promise<void> {
421
- const socialAccounts = (await this.socialAccountRepository.find({
422
- where: { userId },
423
- })) as ISocialAccount[];
424
-
425
- const providers = socialAccounts?.map((account) => account.provider) || [];
426
- const hasSocialAuth = socialAccounts && socialAccounts.length > 0;
427
-
428
- // Use save() instead of update() to ensure TypeORM properly serializes simple-array fields
429
- const user = await this.userRepository.findOne({ where: { id: userId } });
430
- if (user) {
431
- user.hasSocialAuth = hasSocialAuth;
432
- user.socialProviders = providers.length > 0 ? providers : null;
433
- await this.userRepository.save(user);
434
- }
435
- }
436
- }