@nauth-toolkit/core 0.1.0 → 0.1.3

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 +30 -0
  3. package/package.json +7 -2
  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,558 +0,0 @@
1
- import { Repository } from 'typeorm';
2
- import { BaseAuthAudit, BaseUser } from '../entities';
3
- import { IAuthAudit, IUser } from '../interfaces/entities.interface';
4
- import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
5
- import { AuthAuditEventStatus } from '../entities/auth-audit.entity';
6
- import { NAuthLogger } from '../utils/nauth-logger';
7
- import { NAuthException } from '../exceptions/nauth.exception';
8
- import { AuthErrorCode } from '../enums/error-codes.enum';
9
- import { ClientInfoService } from './client-info.service';
10
- import { RiskFactor } from '../enums/risk-factor.enum';
11
- import { GetUserAuthHistoryDTO, GetUserAuthHistoryResponseDTO } from '../dto/get-user-auth-history.dto';
12
- import { GetEventsByTypeDTO, GetEventsByTypeResponseDTO } from '../dto/get-events-by-type.dto';
13
- import { GetSuspiciousActivityDTO, GetSuspiciousActivityResponseDTO } from '../dto/get-suspicious-activity.dto';
14
- import {
15
- GetRiskAssessmentHistoryDTO,
16
- GetRiskAssessmentHistoryResponseDTO,
17
- } from '../dto/get-risk-assessment-history.dto';
18
-
19
- /**
20
- * DTO for creating audit events
21
- *
22
- * @internal
23
- * This DTO is only used by InternalAuthAuditService and should not be exposed
24
- * to consumer applications.
25
- */
26
- export interface CreateAuthAuditEventDTO {
27
- userId?: number; // Internal user ID (preferred)
28
- userSub?: string; // External user identifier (will lookup userId)
29
- eventType: AuthAuditEventType;
30
- eventStatus: AuthAuditEventStatus;
31
- riskFactor?: number | null;
32
- riskFactors?: RiskFactor[] | null;
33
- adaptiveMfaTriggered?: boolean | null;
34
- // Note: ipAddress, ipCountry, ipCity, userAgent, platform, browser, deviceId, deviceName, deviceType
35
- // are automatically captured from ClientInfoService and cannot be overridden
36
- // Use metadata for event-specific data only
37
- deviceId?: string | null; // Special case: can override for newly created device tokens
38
- sessionId?: number | null;
39
- challengeSessionId?: number | null;
40
- authMethod?: string | null;
41
- performedBy?: string | null; // Auto-populated from session context if not provided (userId of authenticated user)
42
- reason?: string | null;
43
- description?: string | null;
44
- metadata?: Record<string, unknown> | null;
45
- }
46
-
47
- /**
48
- * Authentication Audit Service (Base Class - Public API)
49
- *
50
- * Manages audit trail queries for authentication and security events.
51
- * Provides query capabilities for retrieving audit history.
52
- *
53
- * **Key Features:**
54
- * - Efficient queries using userId (internal integer ID)
55
- * - Pagination support for large datasets
56
- * - Query filtering by event type, status, date ranges
57
- * - User history queries (resolves userSub to userId automatically)
58
- *
59
- * **Design Notes:**
60
- * - Only stores `userId` (integer) - no userSub duplication
61
- * - All methods accepting userSub resolve to userId before querying
62
- * - Risk tracking fields are infrastructure for future adaptive MFA (no business logic)
63
- *
64
- * **Note:** This is the public API class. Event recording is handled internally
65
- * by `InternalAuthAuditService` and is not exposed to consumer applications.
66
- *
67
- * @example
68
- * ```typescript
69
- * // Get user history (accepts userSub, resolves to userId)
70
- * const history = await auditService.getUserAuthHistory({
71
- * userSub: 'user-uuid',
72
- * page: 1,
73
- * limit: 50,
74
- * startDate: new Date('2025-01-01'),
75
- * });
76
- * ```
77
- */
78
- export class AuthAuditService {
79
- constructor(
80
- protected readonly auditRepository: Repository<BaseAuthAudit>,
81
- protected readonly userRepository: Repository<BaseUser>,
82
- protected readonly logger: NAuthLogger,
83
- protected readonly clientInfoService?: ClientInfoService,
84
- ) {}
85
-
86
- // ============================================================================
87
- // Query Methods
88
- // ============================================================================
89
-
90
- /**
91
- * Get paginated authentication history for a user
92
- *
93
- * Accepts userSub (external identifier) and resolves to userId for efficient queries.
94
- * Supports filtering by event types, status, and date ranges.
95
- *
96
- * @param request - Request DTO containing userSub and filtering options
97
- * @returns Response DTO with paginated audit records
98
- * @throws {NAuthException} If user not found
99
- *
100
- * @example
101
- * ```typescript
102
- * const history = await auditService.getUserAuthHistory({
103
- * userSub: 'user-uuid',
104
- * page: 1,
105
- * limit: 50,
106
- * eventTypes: [AuthAuditEventType.LOGIN_SUCCESS, AuthAuditEventType.LOGIN_FAILED],
107
- * startDate: new Date('2025-01-01'),
108
- * });
109
- * ```
110
- */
111
- async getUserAuthHistory(request: GetUserAuthHistoryDTO): Promise<GetUserAuthHistoryResponseDTO> {
112
- // Resolve userSub to userId
113
- const user = (await this.userRepository.findOne({ where: { sub: request.userSub } })) as IUser | null;
114
- if (!user) {
115
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
116
- }
117
-
118
- const page = request.page || 1;
119
- const limit = request.limit || 50;
120
- const skip = (page - 1) * limit;
121
-
122
- // Build query
123
- const queryBuilder = this.auditRepository
124
- .createQueryBuilder('audit')
125
- .where('audit.userId = :userId', { userId: user.id });
126
-
127
- // Date range filter
128
- if (request.startDate) {
129
- queryBuilder.andWhere('audit.createdAt >= :startDate', { startDate: request.startDate });
130
- }
131
- if (request.endDate) {
132
- queryBuilder.andWhere('audit.createdAt <= :endDate', { endDate: request.endDate });
133
- }
134
-
135
- // Event type filter
136
- if (request.eventTypes && request.eventTypes.length > 0) {
137
- queryBuilder.andWhere('audit.eventType IN (:...eventTypes)', { eventTypes: request.eventTypes });
138
- }
139
-
140
- // Event status filter
141
- if (request.eventStatus && request.eventStatus.length > 0) {
142
- queryBuilder.andWhere('audit.eventStatus IN (:...eventStatus)', { eventStatus: request.eventStatus });
143
- }
144
-
145
- // Order by date (newest first)
146
- queryBuilder.orderBy('audit.createdAt', 'DESC');
147
-
148
- // Pagination
149
- queryBuilder.skip(skip).take(limit);
150
-
151
- const [data, total] = await queryBuilder.getManyAndCount();
152
-
153
- const response = new GetUserAuthHistoryResponseDTO();
154
- response.data = data as unknown as IAuthAudit[];
155
- response.total = total;
156
- response.page = page;
157
- response.limit = limit;
158
- response.totalPages = Math.ceil(total / limit);
159
-
160
- return response;
161
- }
162
-
163
- /**
164
- * Get events by type with pagination
165
- *
166
- * @param request - Request DTO containing eventType and pagination options
167
- * @returns Response DTO with paginated audit records
168
- *
169
- * @example
170
- * ```typescript
171
- * const events = await auditService.getEventsByType({
172
- * eventType: AuthAuditEventType.SUSPICIOUS_ACTIVITY,
173
- * page: 1,
174
- * limit: 100,
175
- * });
176
- * ```
177
- */
178
- async getEventsByType(request: GetEventsByTypeDTO): Promise<GetEventsByTypeResponseDTO> {
179
- const page = request.page || 1;
180
- const limit = request.limit || 50;
181
- const skip = (page - 1) * limit;
182
-
183
- const queryBuilder = this.auditRepository.createQueryBuilder('audit').where('audit.eventType = :eventType', {
184
- eventType: request.eventType,
185
- });
186
-
187
- // Date range filter
188
- if (request.startDate) {
189
- queryBuilder.andWhere('audit.createdAt >= :startDate', { startDate: request.startDate });
190
- }
191
- if (request.endDate) {
192
- queryBuilder.andWhere('audit.createdAt <= :endDate', { endDate: request.endDate });
193
- }
194
-
195
- queryBuilder.orderBy('audit.createdAt', 'DESC').skip(skip).take(limit);
196
-
197
- const [data, total] = await queryBuilder.getManyAndCount();
198
-
199
- const response = new GetEventsByTypeResponseDTO();
200
- response.data = data as unknown as IAuthAudit[];
201
- response.total = total;
202
- response.page = page;
203
- response.limit = limit;
204
- response.totalPages = Math.ceil(total / limit);
205
-
206
- return response;
207
- }
208
-
209
- /**
210
- * Get suspicious activity events
211
- *
212
- * Returns events with SUSPICIOUS status or SUSPICIOUS_ACTIVITY event type.
213
- *
214
- * @param request - Request DTO containing optional userSub and limit
215
- * @returns Response DTO with array of suspicious audit events
216
- *
217
- * @example
218
- * ```typescript
219
- * // Get all suspicious activity
220
- * const suspicious = await auditService.getSuspiciousActivity({});
221
- *
222
- * // Get suspicious activity for specific user
223
- * const userSuspicious = await auditService.getSuspiciousActivity({
224
- * userSub: 'user-uuid',
225
- * limit: 50,
226
- * });
227
- * ```
228
- */
229
- async getSuspiciousActivity(request: GetSuspiciousActivityDTO): Promise<GetSuspiciousActivityResponseDTO> {
230
- const limit = request.limit || 100;
231
-
232
- const queryBuilder = this.auditRepository
233
- .createQueryBuilder('audit')
234
- .where('(audit.eventStatus = :status OR audit.eventType = :eventType)', {
235
- status: 'SUSPICIOUS',
236
- eventType: AuthAuditEventType.SUSPICIOUS_ACTIVITY,
237
- });
238
-
239
- // Filter by user if provided
240
- if (request.userSub) {
241
- const user = (await this.userRepository.findOne({ where: { sub: request.userSub } })) as IUser | null;
242
- if (!user) {
243
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
244
- }
245
- queryBuilder.andWhere('audit.userId = :userId', { userId: user.id });
246
- }
247
-
248
- queryBuilder.orderBy('audit.createdAt', 'DESC').take(limit);
249
-
250
- const data = await queryBuilder.getMany();
251
-
252
- const response = new GetSuspiciousActivityResponseDTO();
253
- response.data = data as unknown as IAuthAudit[];
254
-
255
- return response;
256
- }
257
-
258
- /**
259
- * Get risk assessment history for adaptive MFA analysis
260
- *
261
- * Returns events where risk assessment was performed (ADAPTIVE_MFA_RISK_ASSESSED,
262
- * ADAPTIVE_MFA_TRIGGERED, ADAPTIVE_MFA_BYPASSED).
263
- *
264
- * @param request - Request DTO containing userSub and limit
265
- * @returns Response DTO with array of risk assessment audit events
266
- * @throws {NAuthException} If user not found
267
- *
268
- * @example
269
- * ```typescript
270
- * const riskHistory = await auditService.getRiskAssessmentHistory({
271
- * userSub: 'user-uuid',
272
- * limit: 50,
273
- * });
274
- * ```
275
- */
276
- async getRiskAssessmentHistory(request: GetRiskAssessmentHistoryDTO): Promise<GetRiskAssessmentHistoryResponseDTO> {
277
- const limit = request.limit || 100;
278
-
279
- // Resolve userSub to userId
280
- const user = (await this.userRepository.findOne({ where: { sub: request.userSub } })) as IUser | null;
281
- if (!user) {
282
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
283
- }
284
-
285
- const queryBuilder = this.auditRepository
286
- .createQueryBuilder('audit')
287
- .where('audit.userId = :userId', { userId: user.id })
288
- .andWhere('audit.eventType IN (:...eventTypes)', {
289
- eventTypes: [
290
- AuthAuditEventType.ADAPTIVE_MFA_RISK_ASSESSED,
291
- AuthAuditEventType.ADAPTIVE_MFA_TRIGGERED,
292
- AuthAuditEventType.ADAPTIVE_MFA_BYPASSED,
293
- ],
294
- })
295
- .orderBy('audit.createdAt', 'DESC')
296
- .take(limit);
297
-
298
- const data = await queryBuilder.getMany();
299
-
300
- const response = new GetRiskAssessmentHistoryResponseDTO();
301
- response.data = data as unknown as IAuthAudit[];
302
-
303
- return response;
304
- }
305
- }
306
-
307
- // ============================================================================
308
- // Internal Service (Framework Adapters Only)
309
- // ============================================================================
310
-
311
- /**
312
- * Internal Authentication Audit Service
313
- *
314
- * Extends the base AuthAuditService with event recording capabilities.
315
- * This service is only available via `@nauth-toolkit/core/internal` and should
316
- * NOT be used by consumer applications.
317
- *
318
- * **Event Recording:**
319
- * The `recordEvent()` method is internal-only and is used by nauth-toolkit
320
- * services to log authentication events. Consumer applications should use
321
- * the query methods from the base `AuthAuditService` class.
322
- *
323
- * @internal
324
- * This class is only exported from `@nauth-toolkit/core/internal` for use
325
- * by framework adapters. Consumer applications should use the base
326
- * `AuthAuditService` from `@nauth-toolkit/core`.
327
- *
328
- * @example
329
- * ```typescript
330
- * // Framework adapter usage
331
- * import { AuthAuditService } from '@nauth-toolkit/core/internal';
332
- *
333
- * const auditService = new AuthAuditService(...);
334
- * // Can use recordEvent() here (internal only)
335
- * await auditService.recordEvent({ ... });
336
- * ```
337
- */
338
- export class InternalAuthAuditService extends AuthAuditService {
339
- /**
340
- * Record an authentication audit event
341
- *
342
- * Creates an audit record for an authentication or security event.
343
- * Automatically extracts client information from request context when available.
344
- * This method is non-blocking - errors are logged but don't throw exceptions.
345
- *
346
- * **Automatic Client Info Extraction:**
347
- * When ClientInfoService is available, the following fields are automatically populated:
348
- * - ipAddress, ipCountry, ipCity (from request and geolocation)
349
- * - userAgent, platform, browser (from user agent parsing)
350
- * - deviceId, deviceName, deviceType (from request context)
351
- *
352
- * Note: These fields cannot be overridden via the DTO - they are always captured from the request context.
353
- * Only deviceId can be explicitly set for special cases (e.g., newly created device tokens).
354
- * Do not include ipAddress, ipCountry, ipCity, userAgent, platform, browser, deviceName, or deviceType
355
- * in the DTO - they will be automatically captured and attempts to include them will cause TypeScript errors.
356
- *
357
- * **Automatic performedBy Population:**
358
- * The `performedBy` field is automatically populated from the authenticated user's context:
359
- * - If userId is available from ClientInfoService (extracted from JWT token by interceptors/handlers), it is used as `performedBy`
360
- * - This captures who performed the action (e.g., admin performing action on another user)
361
- * - If no userId is found in client info, `performedBy` defaults to the event's `userId` (user performing action on themselves)
362
- * - Explicit `performedBy` in DTO overrides automatic population
363
- *
364
- * @internal
365
- * This method is only available in InternalAuthAuditService and should not
366
- * be exposed to consumer applications.
367
- *
368
- * @param data - Audit event data (only event-specific fields needed)
369
- * @param data.userId - Internal user ID (preferred, more efficient)
370
- * @param data.userSub - External user identifier (will lookup userId if userId not provided)
371
- * @param data.eventType - Type of event
372
- * @param data.eventStatus - Event classification status
373
- * @returns Created audit record
374
- *
375
- * @example
376
- * ```typescript
377
- * // Simple recording - client info auto-populated
378
- * await auditService.recordEvent({
379
- * userId: user.id,
380
- * eventType: AuthAuditEventType.LOGIN_SUCCESS,
381
- * eventStatus: 'SUCCESS',
382
- * authMethod: 'password',
383
- * // ipAddress, userAgent, deviceName, etc. automatically included!
384
- * });
385
- *
386
- * // Override specific fields if needed
387
- * await auditService.recordEvent({
388
- * userId: user.id,
389
- * eventType: AuthAuditEventType.LOGIN_SUCCESS,
390
- * eventStatus: 'SUCCESS',
391
- * // ipAddress, userAgent, etc. automatically captured from request context
392
- * });
393
- * ```
394
- */
395
- async recordEvent(data: CreateAuthAuditEventDTO): Promise<IAuthAudit | null> {
396
- try {
397
- // Resolve userId if userSub provided
398
- let userId = data.userId;
399
- if (!userId && data.userSub) {
400
- const user = (await this.userRepository.findOne({ where: { sub: data.userSub } })) as IUser | null;
401
- if (!user) {
402
- this.logger?.warn?.(`Cannot record audit event - user not found: ${data.userSub}`);
403
- return null;
404
- }
405
- userId = user.id;
406
- }
407
-
408
- if (!userId) {
409
- this.logger?.warn?.('Cannot record audit event - userId or userSub required');
410
- return null;
411
- }
412
-
413
- // ============================================================================
414
- // Auto-extract client info from context (when available)
415
- // Note: These fields are automatically captured and cannot be overridden by callers
416
- // ============================================================================
417
- let clientInfo: {
418
- ipAddress?: string | null;
419
- ipCountry?: string | null;
420
- ipCity?: string | null;
421
- ipLatitude?: number | null;
422
- ipLongitude?: number | null;
423
- userAgent?: string | null;
424
- platform?: string | null;
425
- browser?: string | null;
426
- deviceId?: string | null;
427
- deviceName?: string | null;
428
- deviceType?: string | null;
429
- sessionId?: number | null;
430
- } = {};
431
-
432
- if (this.clientInfoService) {
433
- try {
434
- const clientInfoFromContext = this.clientInfoService.get();
435
-
436
- // Debug logging
437
- if (!clientInfoFromContext.ipLatitude || !clientInfoFromContext.ipLongitude) {
438
- this.logger?.warn?.(
439
- `[AuthAuditService] Creating audit WITHOUT coordinates from context: ` +
440
- `IP=${clientInfoFromContext.ipAddress}, country=${clientInfoFromContext.ipCountry}, ` +
441
- `city=${clientInfoFromContext.ipCity}, lat=${clientInfoFromContext.ipLatitude}, ` +
442
- `lon=${clientInfoFromContext.ipLongitude}`,
443
- );
444
- } else {
445
- this.logger?.debug?.(
446
- `[AuthAuditService] Creating audit WITH coordinates from context: ` +
447
- `IP=${clientInfoFromContext.ipAddress}, ${clientInfoFromContext.ipCity}, ` +
448
- `${clientInfoFromContext.ipCountry} (${clientInfoFromContext.ipLatitude}, ${clientInfoFromContext.ipLongitude})`,
449
- );
450
- }
451
-
452
- // Automatically capture from context (no override allowed)
453
- clientInfo = {
454
- ipAddress: clientInfoFromContext.ipAddress || null,
455
- ipCountry: clientInfoFromContext.ipCountry || null,
456
- ipCity: clientInfoFromContext.ipCity || null,
457
- ipLatitude: clientInfoFromContext.ipLatitude || null,
458
- ipLongitude: clientInfoFromContext.ipLongitude || null,
459
- userAgent: clientInfoFromContext.userAgent || null,
460
- platform: clientInfoFromContext.platform || null,
461
- browser: clientInfoFromContext.browser || null,
462
- deviceId: clientInfoFromContext.deviceToken || null,
463
- deviceName: clientInfoFromContext.deviceName || null,
464
- deviceType: clientInfoFromContext.deviceType || null,
465
- sessionId: clientInfoFromContext.sessionId || null,
466
- };
467
- } catch (error) {
468
- // Non-blocking: If client info extraction fails, continue without it
469
- // This can happen if called outside request context (e.g., cron jobs)
470
- this.logger?.debug?.(
471
- `Failed to extract client info for audit: ${error instanceof Error ? error.message : 'Unknown error'}`,
472
- );
473
- }
474
- }
475
-
476
- // ============================================================================
477
- // Use auto-extracted client info (deviceId can be overridden for special cases)
478
- // ============================================================================
479
- const mergedData = {
480
- ipAddress: clientInfo.ipAddress ?? null,
481
- ipCountry: clientInfo.ipCountry ?? null,
482
- ipCity: clientInfo.ipCity ?? null,
483
- ipLatitude: clientInfo.ipLatitude ?? null,
484
- ipLongitude: clientInfo.ipLongitude ?? null,
485
- userAgent: clientInfo.userAgent ?? null,
486
- platform: clientInfo.platform ?? null,
487
- browser: clientInfo.browser ?? null,
488
- deviceId: data.deviceId ?? clientInfo.deviceId ?? null, // Allow override for newly created device tokens
489
- deviceName: clientInfo.deviceName ?? null,
490
- deviceType: clientInfo.deviceType ?? null,
491
- sessionId: data.sessionId ?? clientInfo.sessionId ?? null,
492
- };
493
-
494
- // ============================================================================
495
- // Auto-populate performedBy from client info context (if available)
496
- // ============================================================================
497
- let performedBy: string | null = data.performedBy ?? null;
498
- if (!performedBy && this.clientInfoService) {
499
- try {
500
- // Get userId from client info (extracted from JWT token by interceptors/handlers)
501
- const clientInfo = this.clientInfoService.get();
502
- if (clientInfo?.userId) {
503
- // Use the userId from client info as performedBy
504
- // This captures who performed the action (could be admin performing action on another user)
505
- performedBy = String(clientInfo.userId);
506
- }
507
- } catch (error) {
508
- // Non-blocking: If client info extraction fails, continue without performedBy
509
- this.logger?.debug?.(
510
- `Failed to get userId from client info for performedBy: ${error instanceof Error ? error.message : 'Unknown error'}`,
511
- );
512
- }
513
- }
514
-
515
- // If still no performedBy and we have userId, use it as fallback
516
- // (most actions are performed by the user themselves)
517
- if (!performedBy && userId) {
518
- performedBy = String(userId);
519
- }
520
-
521
- // Create audit record
522
- const auditRecord = this.auditRepository.create({
523
- userId,
524
- eventType: data.eventType,
525
- eventStatus: data.eventStatus,
526
- riskFactor: data.riskFactor ?? null,
527
- riskFactors: data.riskFactors ?? null,
528
- adaptiveMfaTriggered: data.adaptiveMfaTriggered ?? null,
529
- ipAddress: mergedData.ipAddress,
530
- ipCountry: mergedData.ipCountry,
531
- ipCity: mergedData.ipCity,
532
- ipLatitude: mergedData.ipLatitude,
533
- ipLongitude: mergedData.ipLongitude,
534
- userAgent: mergedData.userAgent,
535
- platform: mergedData.platform,
536
- browser: mergedData.browser,
537
- deviceId: mergedData.deviceId,
538
- deviceName: mergedData.deviceName,
539
- deviceType: mergedData.deviceType,
540
- sessionId: mergedData.sessionId,
541
- challengeSessionId: data.challengeSessionId ?? null,
542
- authMethod: data.authMethod ?? null,
543
- performedBy,
544
- reason: data.reason ?? null,
545
- description: data.description ?? null,
546
- metadata: data.metadata ?? null,
547
- });
548
-
549
- const saved = await this.auditRepository.save(auditRecord);
550
- return saved as unknown as IAuthAudit;
551
- } catch (error) {
552
- // Non-blocking: Log error but don't throw
553
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
554
- this.logger?.error?.(`Failed to record audit event: ${errorMessage}`, { eventType: data.eventType, error });
555
- return null;
556
- }
557
- }
558
- }