@bernierllc/auth-suite 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +468 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/managers/audit-manager.d.ts +26 -0
- package/dist/managers/audit-manager.d.ts.map +1 -0
- package/dist/managers/audit-manager.js +160 -0
- package/dist/managers/audit-manager.js.map +1 -0
- package/dist/managers/mfa-manager.d.ts +32 -0
- package/dist/managers/mfa-manager.d.ts.map +1 -0
- package/dist/managers/mfa-manager.js +206 -0
- package/dist/managers/mfa-manager.js.map +1 -0
- package/dist/managers/rbac-manager.d.ts +25 -0
- package/dist/managers/rbac-manager.d.ts.map +1 -0
- package/dist/managers/rbac-manager.js +192 -0
- package/dist/managers/rbac-manager.js.map +1 -0
- package/dist/managers/session-manager.d.ts +20 -0
- package/dist/managers/session-manager.d.ts.map +1 -0
- package/dist/managers/session-manager.js +110 -0
- package/dist/managers/session-manager.js.map +1 -0
- package/dist/suite.d.ts +172 -0
- package/dist/suite.d.ts.map +1 -0
- package/dist/suite.js +1145 -0
- package/dist/suite.js.map +1 -0
- package/dist/types.d.ts +250 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +68 -0
package/dist/suite.js
ADDED
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.AuthSuite = void 0;
|
|
44
|
+
const auth_service_1 = require("@bernierllc/auth-service");
|
|
45
|
+
const logger_1 = require("@bernierllc/logger");
|
|
46
|
+
const email_sender_1 = require("@bernierllc/email-sender");
|
|
47
|
+
const crypto_utils_1 = require("@bernierllc/crypto-utils");
|
|
48
|
+
const session_manager_1 = require("./managers/session-manager");
|
|
49
|
+
const rbac_manager_1 = require("./managers/rbac-manager");
|
|
50
|
+
const mfa_manager_1 = require("./managers/mfa-manager");
|
|
51
|
+
const audit_manager_1 = require("./managers/audit-manager");
|
|
52
|
+
/**
|
|
53
|
+
* Complete authentication suite providing comprehensive auth functionality
|
|
54
|
+
*
|
|
55
|
+
* Features:
|
|
56
|
+
* - Full authentication service capabilities
|
|
57
|
+
* - Enhanced session management with cleanup
|
|
58
|
+
* - Role-based access control (RBAC)
|
|
59
|
+
* - Multi-factor authentication (MFA)
|
|
60
|
+
* - Comprehensive audit logging
|
|
61
|
+
* - OAuth/SAML/LDAP integration support
|
|
62
|
+
* - Advanced security features
|
|
63
|
+
*/
|
|
64
|
+
class AuthSuite {
|
|
65
|
+
authService;
|
|
66
|
+
sessionManager;
|
|
67
|
+
rbacManager;
|
|
68
|
+
mfaManager;
|
|
69
|
+
auditManager;
|
|
70
|
+
logger;
|
|
71
|
+
config;
|
|
72
|
+
emailSender;
|
|
73
|
+
neverhubAdapter;
|
|
74
|
+
verificationTokens = new Map();
|
|
75
|
+
constructor(config) {
|
|
76
|
+
this.config = this.mergeWithDefaults(config);
|
|
77
|
+
this.logger = (0, logger_1.createLogger)({
|
|
78
|
+
level: logger_1.LogLevel.INFO,
|
|
79
|
+
context: { service: 'auth-suite' }
|
|
80
|
+
});
|
|
81
|
+
// Initialize core auth service
|
|
82
|
+
this.authService = new auth_service_1.AuthService(this.config);
|
|
83
|
+
// Initialize managers based on configuration
|
|
84
|
+
this.initializeManagers();
|
|
85
|
+
// Initialize email sender if configured
|
|
86
|
+
if (this.config.email?.enabled && this.config.email.provider) {
|
|
87
|
+
try {
|
|
88
|
+
this.emailSender = new email_sender_1.EmailSender(this.config.email.provider);
|
|
89
|
+
this.logger.info('Email sender initialized successfully');
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.logger.warn('Failed to initialize email sender', { error });
|
|
93
|
+
if (this.config.email.fallback?.disableOnError) {
|
|
94
|
+
this.config.email.enabled = false;
|
|
95
|
+
this.logger.info('Email integration disabled due to initialization error');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Initialize NeverHub adapter if configured
|
|
100
|
+
this.initializeNeverHub();
|
|
101
|
+
this.logger.info('AuthSuite initialized', {
|
|
102
|
+
features: {
|
|
103
|
+
sessions: !!this.config.sessions?.enabled,
|
|
104
|
+
rbac: !!this.config.rbac?.enabled,
|
|
105
|
+
mfa: !!this.config.mfaEnhanced?.enabled,
|
|
106
|
+
auditing: !!this.config.auditing?.enabled,
|
|
107
|
+
email: !!this.config.email?.enabled,
|
|
108
|
+
neverhub: !!this.neverhubAdapter
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Register a new user with enhanced features
|
|
114
|
+
*/
|
|
115
|
+
async register(credentials) {
|
|
116
|
+
try {
|
|
117
|
+
// Audit the registration attempt
|
|
118
|
+
await this.auditManager.log({
|
|
119
|
+
event: 'register_attempt',
|
|
120
|
+
level: 'info',
|
|
121
|
+
details: { email: credentials.email, username: credentials.username }
|
|
122
|
+
});
|
|
123
|
+
// Register user through auth service
|
|
124
|
+
const result = await this.authService.register(credentials);
|
|
125
|
+
if (result.success && result.user) {
|
|
126
|
+
// Create session if enabled
|
|
127
|
+
let session;
|
|
128
|
+
if (this.config.sessions?.enabled !== false) {
|
|
129
|
+
session = await this.sessionManager.create(result.user.id, {
|
|
130
|
+
registrationMethod: 'direct'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Assign default role if RBAC is enabled
|
|
134
|
+
if (this.config.rbac?.enabled && this.config.rbac.roles.length > 0) {
|
|
135
|
+
const defaultRole = this.config.rbac.roles.find(r => r.name === 'user') || this.config.rbac.roles[0];
|
|
136
|
+
await this.rbacManager.assignRole(result.user.id, defaultRole.name);
|
|
137
|
+
}
|
|
138
|
+
// Get user permissions
|
|
139
|
+
const permissions = this.config.rbac?.enabled
|
|
140
|
+
? await this.getUserPermissions(result.user.id)
|
|
141
|
+
: [];
|
|
142
|
+
// Send verification email if email integration is enabled
|
|
143
|
+
let emailVerificationRequired = false;
|
|
144
|
+
let verificationTokenSent = false;
|
|
145
|
+
if (this.shouldSendVerificationEmail()) {
|
|
146
|
+
try {
|
|
147
|
+
const verificationSent = await this.sendVerificationEmail(result.user.id, credentials.email);
|
|
148
|
+
emailVerificationRequired = this.config.email?.verification?.required ?? false;
|
|
149
|
+
verificationTokenSent = verificationSent;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.logger.warn('Failed to send verification email', { error, userId: result.user.id });
|
|
153
|
+
// Handle email error gracefully
|
|
154
|
+
if (this.config.email?.fallback?.disableOnError) {
|
|
155
|
+
this.logger.info('Disabling email verification due to send error');
|
|
156
|
+
emailVerificationRequired = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Send welcome email if configured
|
|
161
|
+
if (this.shouldSendWelcomeEmail('registration')) {
|
|
162
|
+
try {
|
|
163
|
+
await this.sendWelcomeEmail(result.user.id, credentials.email, 'registration');
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
this.logger.warn('Failed to send welcome email', { error, userId: result.user.id });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Audit successful registration
|
|
170
|
+
await this.auditManager.log({
|
|
171
|
+
userId: result.user.id,
|
|
172
|
+
sessionId: session?.sessionId,
|
|
173
|
+
event: 'register',
|
|
174
|
+
level: 'info',
|
|
175
|
+
details: {
|
|
176
|
+
email: credentials.email,
|
|
177
|
+
emailVerificationRequired,
|
|
178
|
+
verificationTokenSent
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
...result,
|
|
183
|
+
session,
|
|
184
|
+
permissions,
|
|
185
|
+
emailVerificationRequired,
|
|
186
|
+
verificationTokenSent
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Audit failed registration
|
|
190
|
+
await this.auditManager.log({
|
|
191
|
+
event: 'register_failed',
|
|
192
|
+
level: 'warn',
|
|
193
|
+
details: { email: credentials.email, error: result.error }
|
|
194
|
+
});
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.logger.error('Registration error', error instanceof Error ? error : undefined);
|
|
199
|
+
await this.auditManager.log({
|
|
200
|
+
event: 'register_error',
|
|
201
|
+
level: 'error',
|
|
202
|
+
details: { error: error instanceof Error ? error.message : String(error) }
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
error: 'Registration failed due to internal error'
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Login user with enhanced security features
|
|
212
|
+
*/
|
|
213
|
+
async login(credentials) {
|
|
214
|
+
try {
|
|
215
|
+
// Audit login attempt
|
|
216
|
+
await this.auditManager.log({
|
|
217
|
+
event: 'login_attempt',
|
|
218
|
+
level: 'info',
|
|
219
|
+
details: { identifier: credentials.email || credentials.username }
|
|
220
|
+
});
|
|
221
|
+
// Attempt authentication
|
|
222
|
+
const result = await this.authService.login(credentials);
|
|
223
|
+
if (result.success && result.user) {
|
|
224
|
+
// Check if MFA is required
|
|
225
|
+
const mfaRequired = this.config.mfaEnhanced?.enabled && await this.isMfaRequired(result.user.id);
|
|
226
|
+
if (mfaRequired) {
|
|
227
|
+
// Generate MFA challenge
|
|
228
|
+
const mfaChallenge = await this.mfaManager.generateChallenge(result.user.id);
|
|
229
|
+
await this.auditManager.log({
|
|
230
|
+
userId: result.user.id,
|
|
231
|
+
event: 'mfa_required',
|
|
232
|
+
level: 'info',
|
|
233
|
+
details: { challengeType: mfaChallenge.type }
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
...result,
|
|
237
|
+
mfaRequired: true,
|
|
238
|
+
mfaChallenge
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// Complete login process
|
|
242
|
+
return await this.completeLogin(result);
|
|
243
|
+
}
|
|
244
|
+
// Audit failed login
|
|
245
|
+
await this.auditManager.log({
|
|
246
|
+
event: 'login_failed',
|
|
247
|
+
level: 'warn',
|
|
248
|
+
details: {
|
|
249
|
+
identifier: credentials.email || credentials.username,
|
|
250
|
+
error: result.error
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
this.logger.error('Login error', error instanceof Error ? error : undefined);
|
|
257
|
+
await this.auditManager.log({
|
|
258
|
+
event: 'login_error',
|
|
259
|
+
level: 'error',
|
|
260
|
+
details: { error: error instanceof Error ? error.message : String(error) }
|
|
261
|
+
});
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: 'Login failed due to internal error'
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Complete MFA verification and finish login
|
|
270
|
+
*/
|
|
271
|
+
async completeMfaLogin(challengeId, response) {
|
|
272
|
+
try {
|
|
273
|
+
const verified = await this.mfaManager.verifyChallenge(challengeId, response);
|
|
274
|
+
if (!verified) {
|
|
275
|
+
await this.auditManager.log({
|
|
276
|
+
event: 'mfa_failed',
|
|
277
|
+
level: 'warn',
|
|
278
|
+
details: { challengeId }
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
error: 'Invalid MFA code'
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
// Get user from challenge (implementation depends on MFA manager)
|
|
286
|
+
// For now, return success - this would need proper user retrieval
|
|
287
|
+
await this.auditManager.log({
|
|
288
|
+
event: 'mfa_success',
|
|
289
|
+
level: 'info',
|
|
290
|
+
details: { challengeId }
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
success: true,
|
|
294
|
+
user: {}, // Would retrieve actual user
|
|
295
|
+
token: '', // Would generate actual token
|
|
296
|
+
refreshToken: ''
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
this.logger.error('MFA completion error', error instanceof Error ? error : undefined);
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: 'MFA verification failed'
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Verify token with enhanced session management
|
|
309
|
+
*/
|
|
310
|
+
async verifyToken(token) {
|
|
311
|
+
try {
|
|
312
|
+
const user = await this.authService.verifyToken(token);
|
|
313
|
+
if (user && this.config.sessions?.enabled !== false) {
|
|
314
|
+
// Update session last accessed time
|
|
315
|
+
// Note: This would require session ID from token or separate lookup
|
|
316
|
+
}
|
|
317
|
+
return user;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
this.logger.error('Token verification error', error instanceof Error ? error : undefined);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Check user permission with RBAC
|
|
326
|
+
*/
|
|
327
|
+
async checkPermission(check) {
|
|
328
|
+
if (!this.config.rbac?.enabled) {
|
|
329
|
+
return { allowed: true, reason: 'RBAC disabled' };
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
return await this.rbacManager.checkPermission(check);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
this.logger.error('Permission check error', error instanceof Error ? error : undefined);
|
|
336
|
+
return { allowed: false, reason: 'Permission check failed' };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Get user roles
|
|
341
|
+
*/
|
|
342
|
+
async getUserRoles(userId) {
|
|
343
|
+
if (!this.config.rbac?.enabled) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
return await this.rbacManager.getUserRoles(userId);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
this.logger.error('Get user roles error', error instanceof Error ? error : undefined);
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Get user permissions (all permissions from all roles)
|
|
356
|
+
*/
|
|
357
|
+
async getUserPermissions(userId) {
|
|
358
|
+
if (!this.config.rbac?.enabled) {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const roles = await this.rbacManager.getUserRoles(userId);
|
|
363
|
+
const permissionSets = await Promise.all(roles.map(role => this.rbacManager.getRolePermissions(role)));
|
|
364
|
+
// Flatten and deduplicate permissions
|
|
365
|
+
return [...new Set(permissionSets.flat())];
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
this.logger.error('Get user permissions error', error instanceof Error ? error : undefined);
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Logout user and cleanup session
|
|
374
|
+
*/
|
|
375
|
+
async logout(sessionId) {
|
|
376
|
+
try {
|
|
377
|
+
if (sessionId && this.config.sessions?.enabled !== false) {
|
|
378
|
+
const destroyed = await this.sessionManager.destroy(sessionId);
|
|
379
|
+
await this.auditManager.log({
|
|
380
|
+
sessionId,
|
|
381
|
+
event: 'logout',
|
|
382
|
+
level: 'info',
|
|
383
|
+
details: { sessionDestroyed: destroyed }
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return { success: true };
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
this.logger.error('Logout error', error instanceof Error ? error : undefined);
|
|
390
|
+
return { success: false };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Setup MFA for user
|
|
395
|
+
*/
|
|
396
|
+
async setupMfa(userId, type) {
|
|
397
|
+
if (!this.config.mfaEnhanced?.enabled) {
|
|
398
|
+
throw new Error('MFA is not enabled');
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const result = await this.mfaManager.setupMfa(userId, type);
|
|
402
|
+
await this.auditManager.log({
|
|
403
|
+
userId,
|
|
404
|
+
event: 'mfa_setup',
|
|
405
|
+
level: 'info',
|
|
406
|
+
details: { type }
|
|
407
|
+
});
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
this.logger.error('MFA setup error', error instanceof Error ? error : undefined);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get audit logs with filtering
|
|
417
|
+
*/
|
|
418
|
+
async getAuditLogs(filters) {
|
|
419
|
+
if (!this.config.auditing?.enabled) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
return await this.auditManager.query(filters);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
this.logger.error('Get audit logs error', error instanceof Error ? error : undefined);
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Cleanup expired sessions and audit logs
|
|
432
|
+
*/
|
|
433
|
+
async cleanup() {
|
|
434
|
+
try {
|
|
435
|
+
const sessionsCleaned = this.config.sessions?.enabled !== false
|
|
436
|
+
? await this.sessionManager.cleanup()
|
|
437
|
+
: 0;
|
|
438
|
+
const auditLogsCleaned = this.config.auditing?.enabled && this.config.auditing.retention?.days
|
|
439
|
+
? await this.auditManager.cleanup(this.config.auditing.retention.days)
|
|
440
|
+
: 0;
|
|
441
|
+
this.logger.info('Cleanup completed', { sessionsCleaned, auditLogsCleaned });
|
|
442
|
+
return { sessions: sessionsCleaned, auditLogs: auditLogsCleaned };
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
this.logger.error('Cleanup error', error instanceof Error ? error : undefined);
|
|
446
|
+
return { sessions: 0, auditLogs: 0 };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Send verification email to user
|
|
451
|
+
*/
|
|
452
|
+
async sendVerificationEmail(userId, email) {
|
|
453
|
+
if (!this.emailSender)
|
|
454
|
+
return false;
|
|
455
|
+
try {
|
|
456
|
+
// Generate verification token
|
|
457
|
+
const token = await (0, crypto_utils_1.generateSessionToken)();
|
|
458
|
+
const expiryHours = this.config.email?.verification?.tokenExpiry ?? 24;
|
|
459
|
+
const expiresAt = new Date(Date.now() + expiryHours * 60 * 60 * 1000);
|
|
460
|
+
// Store token for verification
|
|
461
|
+
this.verificationTokens.set(token, {
|
|
462
|
+
userId,
|
|
463
|
+
token,
|
|
464
|
+
expiresAt,
|
|
465
|
+
type: 'verification'
|
|
466
|
+
});
|
|
467
|
+
// Get template configuration with enhanced defaults
|
|
468
|
+
const template = this.config.email?.templates?.verification;
|
|
469
|
+
const defaultSubject = 'Verify your email address';
|
|
470
|
+
const defaultHtmlContent = `
|
|
471
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
472
|
+
<h1 style="color: #333;">Email Verification Required</h1>
|
|
473
|
+
<p>Hello,</p>
|
|
474
|
+
<p>Please verify your email address by clicking the button below:</p>
|
|
475
|
+
<div style="text-align: center; margin: 20px 0;">
|
|
476
|
+
<a href="{{verificationUrl}}"
|
|
477
|
+
style="background-color: #007bff; color: white; padding: 12px 24px;
|
|
478
|
+
text-decoration: none; border-radius: 4px; display: inline-block;">
|
|
479
|
+
Verify Email Address
|
|
480
|
+
</a>
|
|
481
|
+
</div>
|
|
482
|
+
<p>Or copy and paste this link into your browser:</p>
|
|
483
|
+
<p style="word-break: break-all; color: #666;">{{verificationUrl}}</p>
|
|
484
|
+
<p><strong>This link will expire in {{expiryHours}} hours.</strong></p>
|
|
485
|
+
<hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
|
|
486
|
+
<p style="font-size: 12px; color: #666;">
|
|
487
|
+
If you didn't create an account, you can safely ignore this email.
|
|
488
|
+
</p>
|
|
489
|
+
</div>
|
|
490
|
+
`;
|
|
491
|
+
// Build template variables
|
|
492
|
+
const verificationUrl = this.buildVerificationUrl(token);
|
|
493
|
+
const templateVariables = {
|
|
494
|
+
verificationUrl,
|
|
495
|
+
expiryHours: expiryHours.toString(),
|
|
496
|
+
userId,
|
|
497
|
+
email,
|
|
498
|
+
...template?.customVariables
|
|
499
|
+
};
|
|
500
|
+
// Process template content
|
|
501
|
+
const subject = this.processTemplate(template?.subject || defaultSubject, templateVariables);
|
|
502
|
+
const htmlContent = this.processTemplate(template?.htmlContent || defaultHtmlContent, templateVariables);
|
|
503
|
+
const textContent = template?.textContent ?
|
|
504
|
+
this.processTemplate(template.textContent, templateVariables) :
|
|
505
|
+
`Please verify your email address by visiting: ${verificationUrl} (expires in ${expiryHours} hours)`;
|
|
506
|
+
// Prepare email message with template overrides
|
|
507
|
+
const emailMessage = {
|
|
508
|
+
toEmail: email,
|
|
509
|
+
subject,
|
|
510
|
+
htmlContent,
|
|
511
|
+
textContent,
|
|
512
|
+
fromName: template?.fromName,
|
|
513
|
+
fromEmail: template?.fromEmail,
|
|
514
|
+
replyTo: template?.replyTo
|
|
515
|
+
};
|
|
516
|
+
// Send email with NeverHub integration and fallback logic
|
|
517
|
+
const result = await this.sendEmailWithNeverHubFallback(emailMessage, 'verification');
|
|
518
|
+
if (result.success) {
|
|
519
|
+
this.logger.info('Verification email sent successfully', {
|
|
520
|
+
userId,
|
|
521
|
+
email,
|
|
522
|
+
tokenExpiry: expiresAt.toISOString()
|
|
523
|
+
});
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
this.logger.error('Failed to send verification email', undefined, {
|
|
528
|
+
userId,
|
|
529
|
+
email,
|
|
530
|
+
errorMessage: result.errorMessage
|
|
531
|
+
});
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
this.logger.error('Error sending verification email', error instanceof Error ? error : undefined, { userId, email });
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Manually send verification email to a user
|
|
542
|
+
*/
|
|
543
|
+
async sendVerificationEmailManually(userId, email) {
|
|
544
|
+
if (!this.config.email?.enabled) {
|
|
545
|
+
return { success: false, error: 'Email functionality is disabled' };
|
|
546
|
+
}
|
|
547
|
+
if (!this.config.email.verification?.enabled) {
|
|
548
|
+
return { success: false, error: 'Email verification is disabled' };
|
|
549
|
+
}
|
|
550
|
+
if (!this.emailSender) {
|
|
551
|
+
return { success: false, error: 'Email sender not configured' };
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
await this.auditManager.log({
|
|
555
|
+
userId,
|
|
556
|
+
event: 'manual_verification_email_sent',
|
|
557
|
+
level: 'info',
|
|
558
|
+
details: { email }
|
|
559
|
+
});
|
|
560
|
+
const result = await this.sendVerificationEmail(userId, email);
|
|
561
|
+
return { success: result };
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
this.logger.error('Error sending manual verification email', error instanceof Error ? error : undefined, { userId, email });
|
|
565
|
+
return {
|
|
566
|
+
success: false,
|
|
567
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Get email configuration status
|
|
573
|
+
*/
|
|
574
|
+
getEmailConfiguration() {
|
|
575
|
+
return {
|
|
576
|
+
enabled: this.config.email?.enabled ?? false,
|
|
577
|
+
verification: {
|
|
578
|
+
enabled: this.config.email?.verification?.enabled ?? false,
|
|
579
|
+
required: this.config.email?.verification?.required ?? false,
|
|
580
|
+
automaticSend: this.config.email?.verification?.automaticSend ?? false
|
|
581
|
+
},
|
|
582
|
+
welcome: {
|
|
583
|
+
enabled: this.config.email?.welcome?.enabled ?? false,
|
|
584
|
+
sendOnRegistration: this.config.email?.welcome?.sendOnRegistration ?? false,
|
|
585
|
+
sendOnFirstLogin: this.config.email?.welcome?.sendOnFirstLogin ?? false
|
|
586
|
+
},
|
|
587
|
+
passwordReset: {
|
|
588
|
+
enabled: this.config.email?.passwordReset?.enabled ?? false
|
|
589
|
+
},
|
|
590
|
+
providerConfigured: !!this.emailSender,
|
|
591
|
+
neverhub: {
|
|
592
|
+
enabled: this.config.email?.neverhub?.enabled ?? false,
|
|
593
|
+
available: !!this.neverhubAdapter,
|
|
594
|
+
overrideDirect: this.config.email?.neverhub?.overrideDirect ?? false,
|
|
595
|
+
fallbackToDirect: this.config.email?.neverhub?.fallbackToDirect ?? true,
|
|
596
|
+
serviceName: this.config.email?.neverhub?.serviceName ?? 'email'
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Verify user email with token
|
|
602
|
+
*/
|
|
603
|
+
async verifyEmail(token) {
|
|
604
|
+
const tokenData = this.verificationTokens.get(token);
|
|
605
|
+
if (!tokenData) {
|
|
606
|
+
return {
|
|
607
|
+
success: false,
|
|
608
|
+
error: 'Invalid verification token'
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (tokenData.expiresAt < new Date()) {
|
|
612
|
+
this.verificationTokens.delete(token);
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: 'Verification token has expired'
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (tokenData.type !== 'verification') {
|
|
619
|
+
return {
|
|
620
|
+
success: false,
|
|
621
|
+
error: 'Invalid token type'
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
// Mark user as verified (this would depend on your user storage implementation)
|
|
626
|
+
// For now, we'll just return success
|
|
627
|
+
this.verificationTokens.delete(token);
|
|
628
|
+
await this.auditManager.log({
|
|
629
|
+
userId: tokenData.userId,
|
|
630
|
+
event: 'email_verified',
|
|
631
|
+
level: 'info',
|
|
632
|
+
details: { token: token.substring(0, 8) + '...' }
|
|
633
|
+
});
|
|
634
|
+
return {
|
|
635
|
+
success: true,
|
|
636
|
+
verified: true
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
this.logger.error('Email verification error', err instanceof Error ? err : new Error(String(err)), { userId: tokenData.userId });
|
|
641
|
+
return {
|
|
642
|
+
success: false,
|
|
643
|
+
error: 'Email verification failed'
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Send password reset email
|
|
649
|
+
*/
|
|
650
|
+
async sendPasswordResetEmail(email) {
|
|
651
|
+
if (!this.emailSender) {
|
|
652
|
+
return { success: false, error: 'Email functionality not configured' };
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
// This would typically look up user by email first
|
|
656
|
+
// For now, we'll generate a reset token
|
|
657
|
+
const token = await (0, crypto_utils_1.generateSessionToken)();
|
|
658
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
|
659
|
+
// Store token for password reset
|
|
660
|
+
this.verificationTokens.set(token, {
|
|
661
|
+
userId: 'unknown', // Would be actual user ID
|
|
662
|
+
token,
|
|
663
|
+
expiresAt,
|
|
664
|
+
type: 'password_reset'
|
|
665
|
+
});
|
|
666
|
+
// Get template configuration
|
|
667
|
+
const template = this.config.email?.templates?.passwordReset;
|
|
668
|
+
const subject = template?.subject || 'Reset your password';
|
|
669
|
+
const htmlContent = template?.htmlContent || `
|
|
670
|
+
<h1>Password Reset</h1>
|
|
671
|
+
<p>Click the link below to reset your password:</p>
|
|
672
|
+
<a href="{{resetUrl}}">Reset Password</a>
|
|
673
|
+
<p>This link will expire in 1 hour.</p>
|
|
674
|
+
`;
|
|
675
|
+
// Prepare email message
|
|
676
|
+
const emailMessage = {
|
|
677
|
+
toEmail: email,
|
|
678
|
+
subject,
|
|
679
|
+
htmlContent: htmlContent.replace('{{resetUrl}}', `${this.config.email?.provider?.endpoint || ''}/reset?token=${token}`),
|
|
680
|
+
textContent: template?.textContent
|
|
681
|
+
};
|
|
682
|
+
// Send email
|
|
683
|
+
const result = await this.emailSender.sendEmail(emailMessage);
|
|
684
|
+
if (result.success) {
|
|
685
|
+
this.logger.info('Password reset email sent', { email });
|
|
686
|
+
await this.auditManager.log({
|
|
687
|
+
event: 'password_reset_requested',
|
|
688
|
+
level: 'info',
|
|
689
|
+
details: { email }
|
|
690
|
+
});
|
|
691
|
+
return { success: true };
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
this.logger.error('Failed to send password reset email', undefined, { emailAddress: email, errorMessage: result.errorMessage });
|
|
695
|
+
return { success: false, error: result.errorMessage };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
this.logger.error('Error sending password reset email', err instanceof Error ? err : new Error(String(err)), { emailAddress: email });
|
|
700
|
+
return { success: false, error: 'Failed to send password reset email' };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Private methods
|
|
704
|
+
mergeWithDefaults(config) {
|
|
705
|
+
return {
|
|
706
|
+
...config,
|
|
707
|
+
sessions: {
|
|
708
|
+
enabled: true,
|
|
709
|
+
storage: 'memory',
|
|
710
|
+
cleanup: {
|
|
711
|
+
enabled: true,
|
|
712
|
+
intervalMs: 60000, // 1 minute
|
|
713
|
+
maxIdleMs: 24 * 60 * 60 * 1000 // 24 hours
|
|
714
|
+
},
|
|
715
|
+
persistence: {
|
|
716
|
+
enabled: false,
|
|
717
|
+
ttl: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
718
|
+
},
|
|
719
|
+
...config.sessions
|
|
720
|
+
},
|
|
721
|
+
rbac: {
|
|
722
|
+
enabled: false,
|
|
723
|
+
roles: [],
|
|
724
|
+
permissions: [],
|
|
725
|
+
hierarchical: false,
|
|
726
|
+
...config.rbac
|
|
727
|
+
},
|
|
728
|
+
mfaEnhanced: {
|
|
729
|
+
enabled: false,
|
|
730
|
+
providers: [],
|
|
731
|
+
requirements: [],
|
|
732
|
+
backup: {
|
|
733
|
+
enabled: true,
|
|
734
|
+
codeCount: 10,
|
|
735
|
+
codeLength: 8
|
|
736
|
+
},
|
|
737
|
+
...config.mfaEnhanced
|
|
738
|
+
},
|
|
739
|
+
auditing: {
|
|
740
|
+
enabled: true,
|
|
741
|
+
events: [{
|
|
742
|
+
type: 'login',
|
|
743
|
+
level: 'info',
|
|
744
|
+
includeMetadata: true
|
|
745
|
+
}, {
|
|
746
|
+
type: 'logout',
|
|
747
|
+
level: 'info',
|
|
748
|
+
includeMetadata: true
|
|
749
|
+
}, {
|
|
750
|
+
type: 'register',
|
|
751
|
+
level: 'info',
|
|
752
|
+
includeMetadata: true
|
|
753
|
+
}, {
|
|
754
|
+
type: 'password_change',
|
|
755
|
+
level: 'info',
|
|
756
|
+
includeMetadata: true
|
|
757
|
+
}],
|
|
758
|
+
storage: 'file',
|
|
759
|
+
retention: {
|
|
760
|
+
days: 90,
|
|
761
|
+
maxSize: '100MB'
|
|
762
|
+
},
|
|
763
|
+
...config.auditing
|
|
764
|
+
},
|
|
765
|
+
email: config.email ? {
|
|
766
|
+
enabled: config.email.enabled || false,
|
|
767
|
+
provider: config.email.provider,
|
|
768
|
+
neverhub: {
|
|
769
|
+
enabled: false,
|
|
770
|
+
serviceName: 'email',
|
|
771
|
+
overrideDirect: true,
|
|
772
|
+
fallbackToDirect: true,
|
|
773
|
+
timeout: 5000,
|
|
774
|
+
...config.email.neverhub
|
|
775
|
+
},
|
|
776
|
+
verification: {
|
|
777
|
+
enabled: true,
|
|
778
|
+
required: false,
|
|
779
|
+
tokenExpiry: 24,
|
|
780
|
+
automaticSend: true,
|
|
781
|
+
...config.email.verification
|
|
782
|
+
},
|
|
783
|
+
passwordReset: {
|
|
784
|
+
enabled: true,
|
|
785
|
+
tokenExpiry: 1,
|
|
786
|
+
...config.email.passwordReset
|
|
787
|
+
},
|
|
788
|
+
welcome: {
|
|
789
|
+
enabled: false,
|
|
790
|
+
sendOnRegistration: false,
|
|
791
|
+
sendOnFirstLogin: false,
|
|
792
|
+
...config.email.welcome
|
|
793
|
+
},
|
|
794
|
+
templates: {
|
|
795
|
+
verification: {
|
|
796
|
+
subject: 'Verify your email address',
|
|
797
|
+
htmlContent: '<h1>Email Verification</h1><p>Please verify your email address by clicking the link: {{verificationUrl}}</p>',
|
|
798
|
+
textContent: 'Please verify your email address by visiting: {{verificationUrl}}'
|
|
799
|
+
},
|
|
800
|
+
passwordReset: {
|
|
801
|
+
subject: 'Reset your password',
|
|
802
|
+
htmlContent: '<h1>Password Reset</h1><p>Click the link to reset your password: {{resetUrl}}</p>',
|
|
803
|
+
textContent: 'Click the link to reset your password: {{resetUrl}}'
|
|
804
|
+
},
|
|
805
|
+
welcome: {
|
|
806
|
+
subject: 'Welcome!',
|
|
807
|
+
htmlContent: '<h1>Welcome</h1><p>Thank you for joining us!</p>',
|
|
808
|
+
textContent: 'Welcome! Thank you for joining us!'
|
|
809
|
+
},
|
|
810
|
+
...config.email.templates
|
|
811
|
+
},
|
|
812
|
+
fallback: {
|
|
813
|
+
disableOnError: false,
|
|
814
|
+
retryAttempts: 3,
|
|
815
|
+
...config.email.fallback
|
|
816
|
+
}
|
|
817
|
+
} : undefined
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Initialize NeverHub adapter if configured and available
|
|
822
|
+
*/
|
|
823
|
+
async initializeNeverHub() {
|
|
824
|
+
if (!this.config.email?.neverhub?.enabled) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
// Try to dynamically import NeverHub adapter
|
|
829
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
830
|
+
const neverhubModule = await Promise.resolve(`${'@bernierllc/neverhub-adapter'}`).then(s => __importStar(require(s))).catch(() => null);
|
|
831
|
+
if (!neverhubModule) {
|
|
832
|
+
this.logger.info('NeverHub adapter not available, using direct email integration only');
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const { NeverHubAdapter: Adapter } = neverhubModule;
|
|
836
|
+
// Check if NeverHub is available
|
|
837
|
+
if (await Adapter.detect()) {
|
|
838
|
+
this.neverhubAdapter = new Adapter();
|
|
839
|
+
// Register with NeverHub
|
|
840
|
+
await this.neverhubAdapter.register({
|
|
841
|
+
type: 'auth-service',
|
|
842
|
+
name: '@bernierllc/auth-suite',
|
|
843
|
+
version: '1.0.0',
|
|
844
|
+
capabilities: [
|
|
845
|
+
{ type: 'email', name: 'email-sending', version: '1.0.0' }
|
|
846
|
+
],
|
|
847
|
+
dependencies: [this.config.email.neverhub.serviceName || 'email']
|
|
848
|
+
});
|
|
849
|
+
this.logger.info('NeverHub adapter initialized successfully for auth-suite', {
|
|
850
|
+
serviceName: this.config.email.neverhub.serviceName || 'email'
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
this.logger.info('NeverHub not available, using direct email integration only');
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
this.logger.warn('Failed to initialize NeverHub adapter', { error });
|
|
859
|
+
if (!this.config.email.neverhub.fallbackToDirect) {
|
|
860
|
+
this.logger.warn('NeverHub fallback disabled, email functionality may be limited');
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
initializeManagers() {
|
|
865
|
+
// Initialize session manager
|
|
866
|
+
this.sessionManager = new session_manager_1.MemorySessionManager(this.config.sessions || {});
|
|
867
|
+
// Initialize RBAC manager
|
|
868
|
+
this.rbacManager = new rbac_manager_1.BasicRBACManager(this.config.rbac || { enabled: false, roles: [], permissions: [], hierarchical: false });
|
|
869
|
+
// Initialize MFA manager
|
|
870
|
+
this.mfaManager = new mfa_manager_1.TOTPMfaManager(this.config.mfaEnhanced || { enabled: false, providers: [], requirements: [], backup: { enabled: false, codeCount: 0, codeLength: 0 } });
|
|
871
|
+
// Initialize audit manager
|
|
872
|
+
this.auditManager = new audit_manager_1.FileAuditManager(this.config.auditing || { enabled: false, events: [], storage: 'file', retention: { days: 0, maxSize: '' } });
|
|
873
|
+
}
|
|
874
|
+
async completeLogin(result) {
|
|
875
|
+
// Create session
|
|
876
|
+
let session;
|
|
877
|
+
if (this.config.sessions?.enabled !== false) {
|
|
878
|
+
session = await this.sessionManager.create(result.user.id, {
|
|
879
|
+
loginMethod: 'password'
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
// Get permissions
|
|
883
|
+
const permissions = this.config.rbac?.enabled
|
|
884
|
+
? await this.getUserPermissions(result.user.id)
|
|
885
|
+
: [];
|
|
886
|
+
// Send welcome email if configured for first login
|
|
887
|
+
if (this.shouldSendWelcomeEmail('login')) {
|
|
888
|
+
try {
|
|
889
|
+
await this.sendWelcomeEmail(result.user.id, result.user.email, 'login');
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
this.logger.warn('Failed to send welcome email on login', { error, userId: result.user.id });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// Audit successful login
|
|
896
|
+
await this.auditManager.log({
|
|
897
|
+
userId: result.user.id,
|
|
898
|
+
sessionId: session?.sessionId,
|
|
899
|
+
event: 'login',
|
|
900
|
+
level: 'info',
|
|
901
|
+
details: { method: 'password' }
|
|
902
|
+
});
|
|
903
|
+
return {
|
|
904
|
+
...result,
|
|
905
|
+
session,
|
|
906
|
+
permissions
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
async isMfaRequired(userId) {
|
|
910
|
+
// Check MFA requirements based on configuration
|
|
911
|
+
// This is a simplified implementation
|
|
912
|
+
return this.config.mfaEnhanced?.requirements.some(req => req.trigger === 'login') || false;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Check if verification email should be sent
|
|
916
|
+
*/
|
|
917
|
+
shouldSendVerificationEmail() {
|
|
918
|
+
return !!(this.emailSender &&
|
|
919
|
+
this.config.email?.enabled &&
|
|
920
|
+
this.config.email.verification?.enabled !== false &&
|
|
921
|
+
this.config.email.verification?.automaticSend !== false);
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Check if welcome email should be sent
|
|
925
|
+
*/
|
|
926
|
+
shouldSendWelcomeEmail(trigger) {
|
|
927
|
+
if (!this.emailSender || !this.config.email?.enabled || !this.config.email.welcome?.enabled) {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
if (trigger === 'registration') {
|
|
931
|
+
return this.config.email.welcome.sendOnRegistration !== false;
|
|
932
|
+
}
|
|
933
|
+
if (trigger === 'login') {
|
|
934
|
+
return this.config.email.welcome.sendOnFirstLogin === true;
|
|
935
|
+
}
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Build verification URL with token
|
|
940
|
+
*/
|
|
941
|
+
buildVerificationUrl(token) {
|
|
942
|
+
const baseUrl = this.config.email?.provider?.endpoint || 'https://example.com';
|
|
943
|
+
return `${baseUrl}/verify?token=${token}`;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Process template variables in content
|
|
947
|
+
*/
|
|
948
|
+
processTemplate(content, variables) {
|
|
949
|
+
let processed = content;
|
|
950
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
951
|
+
const pattern = new RegExp(`{{${key}}}`, 'g');
|
|
952
|
+
processed = processed.replace(pattern, value || '');
|
|
953
|
+
});
|
|
954
|
+
return processed;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Send email with NeverHub integration and fallback logic
|
|
958
|
+
*/
|
|
959
|
+
async sendEmailWithNeverHubFallback(emailMessage, type) {
|
|
960
|
+
// Check if NeverHub should be used
|
|
961
|
+
if (this.shouldUseNeverHub()) {
|
|
962
|
+
try {
|
|
963
|
+
this.logger.debug('Attempting to send email via NeverHub', { type, toEmail: emailMessage.toEmail });
|
|
964
|
+
const neverhubResult = await this.sendEmailViaNeverHub(emailMessage, type);
|
|
965
|
+
if (neverhubResult.success) {
|
|
966
|
+
this.logger.info('Email sent successfully via NeverHub', { type, method: 'neverhub' });
|
|
967
|
+
return { ...neverhubResult, method: 'neverhub' };
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
this.logger.warn('NeverHub email sending failed', {
|
|
971
|
+
type,
|
|
972
|
+
error: neverhubResult.errorMessage,
|
|
973
|
+
fallbackEnabled: this.config.email?.neverhub?.fallbackToDirect
|
|
974
|
+
});
|
|
975
|
+
// Fall back to direct email if configured
|
|
976
|
+
if (this.config.email?.neverhub?.fallbackToDirect && this.emailSender) {
|
|
977
|
+
this.logger.info('Falling back to direct email sending', { type });
|
|
978
|
+
return this.sendEmailWithRetry(emailMessage, type);
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
return neverhubResult;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
this.logger.error('Error sending email via NeverHub', err instanceof Error ? err : new Error(String(err)), { emailType: type });
|
|
987
|
+
// Fall back to direct email if configured
|
|
988
|
+
if (this.config.email?.neverhub?.fallbackToDirect && this.emailSender) {
|
|
989
|
+
this.logger.info('Falling back to direct email due to NeverHub error', { type });
|
|
990
|
+
return this.sendEmailWithRetry(emailMessage, type);
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
return {
|
|
994
|
+
success: false,
|
|
995
|
+
errorMessage: err instanceof Error ? err.message : 'NeverHub email error'
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// Use direct email sending
|
|
1001
|
+
if (this.emailSender) {
|
|
1002
|
+
return this.sendEmailWithRetry(emailMessage, type);
|
|
1003
|
+
}
|
|
1004
|
+
return { success: false, errorMessage: 'No email sending method available' };
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Send email via NeverHub service discovery
|
|
1008
|
+
*/
|
|
1009
|
+
async sendEmailViaNeverHub(emailMessage, type) {
|
|
1010
|
+
if (!this.neverhubAdapter) {
|
|
1011
|
+
return { success: false, errorMessage: 'NeverHub adapter not initialized' };
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const serviceName = this.config.email?.neverhub?.serviceName || 'email';
|
|
1015
|
+
const timeout = this.config.email?.neverhub?.timeout || 5000;
|
|
1016
|
+
// Get email service from NeverHub
|
|
1017
|
+
const emailService = await this.neverhubAdapter.getService(serviceName);
|
|
1018
|
+
if (!emailService) {
|
|
1019
|
+
return { success: false, errorMessage: `Email service '${serviceName}' not found in NeverHub` };
|
|
1020
|
+
}
|
|
1021
|
+
// Send email message via NeverHub
|
|
1022
|
+
const result = await Promise.race([
|
|
1023
|
+
this.neverhubAdapter.publishEvent({
|
|
1024
|
+
type: 'email.send',
|
|
1025
|
+
data: {
|
|
1026
|
+
...emailMessage,
|
|
1027
|
+
metadata: {
|
|
1028
|
+
source: 'auth-suite',
|
|
1029
|
+
emailType: type,
|
|
1030
|
+
timestamp: new Date().toISOString()
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
targetService: serviceName
|
|
1034
|
+
}),
|
|
1035
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('NeverHub email timeout')), timeout))
|
|
1036
|
+
]);
|
|
1037
|
+
return { success: true };
|
|
1038
|
+
}
|
|
1039
|
+
catch (error) {
|
|
1040
|
+
return {
|
|
1041
|
+
success: false,
|
|
1042
|
+
errorMessage: error instanceof Error ? error.message : 'NeverHub communication error'
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Check if NeverHub should be used for email sending
|
|
1048
|
+
*/
|
|
1049
|
+
shouldUseNeverHub() {
|
|
1050
|
+
return !!(this.neverhubAdapter &&
|
|
1051
|
+
this.config.email?.neverhub?.enabled &&
|
|
1052
|
+
this.config.email.neverhub.overrideDirect);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Send email with retry logic
|
|
1056
|
+
*/
|
|
1057
|
+
async sendEmailWithRetry(emailMessage, type) {
|
|
1058
|
+
const maxRetries = this.config.email?.fallback?.retryAttempts ?? 3;
|
|
1059
|
+
let lastError;
|
|
1060
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1061
|
+
try {
|
|
1062
|
+
const result = await this.emailSender.sendEmail(emailMessage);
|
|
1063
|
+
if (result.success) {
|
|
1064
|
+
if (attempt > 1) {
|
|
1065
|
+
this.logger.info(`Email sent successfully on attempt ${attempt}`, { type });
|
|
1066
|
+
}
|
|
1067
|
+
return { ...result, method: 'direct' };
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
lastError = result.errorMessage;
|
|
1071
|
+
if (attempt < maxRetries) {
|
|
1072
|
+
this.logger.warn(`Email send attempt ${attempt} failed, retrying...`, {
|
|
1073
|
+
type,
|
|
1074
|
+
error: result.errorMessage
|
|
1075
|
+
});
|
|
1076
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1082
|
+
if (attempt < maxRetries) {
|
|
1083
|
+
this.logger.warn(`Email send attempt ${attempt} error, retrying...`, { type, error });
|
|
1084
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
this.logger.error(`Failed to send email after ${maxRetries} attempts`, undefined, { emailType: type, lastError });
|
|
1089
|
+
return { success: false, errorMessage: lastError, method: 'direct' };
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Send welcome email to user
|
|
1093
|
+
*/
|
|
1094
|
+
async sendWelcomeEmail(userId, email, trigger) {
|
|
1095
|
+
if (!this.emailSender)
|
|
1096
|
+
return false;
|
|
1097
|
+
try {
|
|
1098
|
+
const template = this.config.email?.templates?.welcome;
|
|
1099
|
+
const defaultSubject = 'Welcome to our platform!';
|
|
1100
|
+
const defaultHtmlContent = `
|
|
1101
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
1102
|
+
<h1 style="color: #333;">Welcome!</h1>
|
|
1103
|
+
<p>Thank you for joining our platform. We're excited to have you on board!</p>
|
|
1104
|
+
<p>If you have any questions, feel free to reach out to our support team.</p>
|
|
1105
|
+
<p>Best regards,<br>The Team</p>
|
|
1106
|
+
</div>
|
|
1107
|
+
`;
|
|
1108
|
+
const templateVariables = {
|
|
1109
|
+
userId,
|
|
1110
|
+
email,
|
|
1111
|
+
trigger,
|
|
1112
|
+
...template?.customVariables
|
|
1113
|
+
};
|
|
1114
|
+
const subject = this.processTemplate(template?.subject || defaultSubject, templateVariables);
|
|
1115
|
+
const htmlContent = this.processTemplate(template?.htmlContent || defaultHtmlContent, templateVariables);
|
|
1116
|
+
const textContent = template?.textContent ?
|
|
1117
|
+
this.processTemplate(template.textContent, templateVariables) :
|
|
1118
|
+
'Welcome to our platform! Thank you for joining us.';
|
|
1119
|
+
const emailMessage = {
|
|
1120
|
+
toEmail: email,
|
|
1121
|
+
subject,
|
|
1122
|
+
htmlContent,
|
|
1123
|
+
textContent,
|
|
1124
|
+
fromName: template?.fromName,
|
|
1125
|
+
fromEmail: template?.fromEmail,
|
|
1126
|
+
replyTo: template?.replyTo
|
|
1127
|
+
};
|
|
1128
|
+
const result = await this.sendEmailWithNeverHubFallback(emailMessage, 'welcome');
|
|
1129
|
+
if (result.success) {
|
|
1130
|
+
this.logger.info('Welcome email sent successfully', { userIdValue: userId, emailAddress: email, triggerType: trigger });
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
this.logger.error('Failed to send welcome email', undefined, { userIdValue: userId, emailAddress: email, errorMessage: result.errorMessage });
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
this.logger.error('Error sending welcome email', err instanceof Error ? err : new Error(String(err)), { userIdValue: userId, emailAddress: email });
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
exports.AuthSuite = AuthSuite;
|
|
1145
|
+
//# sourceMappingURL=suite.js.map
|