@dax-crafta/auth 2.1.6 → 2.1.8

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/src/index.js CHANGED
@@ -1,442 +1,669 @@
1
- const passport = require('passport');
2
- const cookieParser = require('cookie-parser');
3
- const csrf = require('csurf');
4
- const helmet = require("helmet");
5
- const mongoose = require("mongoose"); // <<< FIX ADDED
6
-
7
- const GoogleStrategy = require('passport-google-oauth20').Strategy;
8
- const AuthService = require('./services/auth.service');
9
- const RoleService = require('./services/role.service');
10
- const AuditService = require('./services/audit.service');
11
- const MFAService = require('./utils/mfa');
12
- const PasswordPolicy = require('./utils/password-policy');
13
- const createAuthMiddleware = require('./middlewares/auth.middleware');
14
- const validators = require('./middlewares/validation.middleware');
15
- const { createLogger } = require('./utils/logger');
16
- const ApiError = require('./utils/api-error');
17
- const { runAdaptiveTests, inferFeatureFlags, testCatalog } = require('./testing/adaptive-runner');
18
-
19
-
20
- const defaultConfig = {
21
- strategy: 'jwt',
22
- fields: ['email', 'password'],
23
- routes: {
24
- register: '/register',
25
- login: '/login',
26
- verify: '/verify',
27
- forgotPassword: '/forgot-password',
28
- resetPassword: '/reset-password',
29
- refreshToken: '/refresh-token',
30
- profile: '/profile',
31
- twoFactor: '/2fa',
32
- roles: '/roles',
33
- permissions: '/permissions'
34
- },
35
- mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/forge-auth',
36
- maxLoginAttempts: 5,
37
- emailVerification: true,
38
- loginAlerts: true,
39
- passwordPolicy: {
40
- minLength: 8,
41
- requireUppercase: true,
42
- requireNumbers: true,
43
- requireSpecialChars: true,
44
- expiryDays: 90
45
- },
46
- smtp: null,
47
- social: {
48
- google: null,
49
- facebook: null,
50
- github: null
51
- },
52
- features: {
53
- emailVerification: true,
54
- loginAlerts: true,
55
- securityAttempts: true,
56
- rateLimit: true,
57
- auditLogs: true,
58
- twoFactor: true,
59
- csrf: false
60
- },
61
- env: {
62
- JWT_SECRET: process.env.JWT_SECRET || 'crafta-auth-dev-secret'
63
- },
64
- accessTokenExpiry: '1h',
65
- refreshTokenDays: 7
66
- };
67
-
68
- function auth(config = {}) {
69
- const finalConfig = {
70
- ...defaultConfig,
71
- ...config,
72
- routes: { ...defaultConfig.routes, ...(config.routes || {}) },
73
- passwordPolicy: { ...defaultConfig.passwordPolicy, ...(config.passwordPolicy || {}) },
74
- social: { ...defaultConfig.social, ...(config.social || {}) },
75
- limits: { ...(config.limits || {}) },
76
- features: { ...defaultConfig.features, ...(config.features || {}) }
77
- };
78
-
79
- finalConfig.env = { ...(defaultConfig.env || {}), ...(config.env || process.env) };
80
- const logger = createLogger(finalConfig.logging !== false);
81
-
82
- const featureBools = {
83
- emailVerification: config.emailVerification,
84
- loginAlerts: config.loginAlerts,
85
- csrf: config.enableCSRF
86
- };
87
-
88
- Object.keys(featureBools).forEach((key) => {
89
- if (typeof featureBools[key] === 'boolean') {
90
- finalConfig.features[key] = featureBools[key];
91
- }
92
- });
93
-
94
- finalConfig.emailVerification = finalConfig.features.emailVerification;
95
- finalConfig.loginAlerts = finalConfig.features.loginAlerts;
96
- finalConfig.enableCSRF = finalConfig.features.csrf;
97
-
98
- if (finalConfig.features.securityAttempts === false) {
99
- finalConfig.loginAlerts = false;
100
- finalConfig.features.loginAlerts = false;
101
- }
102
-
103
- if (finalConfig.env?.JWT_SECRET === 'crafta-auth-dev-secret') {
104
- logger.logWarn('JWT_SECRET not provided; using development default secret. Set env.JWT_SECRET in production.');
105
- }
106
-
107
- const smtpEnabled = !!(
108
- finalConfig.smtp &&
109
- finalConfig.smtp.host &&
110
- finalConfig.smtp.port &&
111
- finalConfig.smtp.auth &&
112
- finalConfig.smtp.auth.user &&
113
- finalConfig.smtp.auth.pass &&
114
- finalConfig.smtp.from
115
- );
116
-
117
- if (!smtpEnabled && finalConfig.features.emailVerification) {
118
- logger.logWarn('SMTP not configured; emailVerification auto-disabled.');
119
- finalConfig.features.emailVerification = false;
120
- finalConfig.emailVerification = false;
121
- }
122
-
123
- if (!smtpEnabled && finalConfig.features.loginAlerts) {
124
- logger.logWarn('SMTP not configured; loginAlerts auto-disabled.');
125
- finalConfig.features.loginAlerts = false;
126
- finalConfig.loginAlerts = false;
127
- }
128
-
129
- // ------------------------------------------
130
- // ✅ Proper MongoDB Connection
131
- // ------------------------------------------
132
- if (mongoose.connection.readyState === 0) {
133
- mongoose.connect(finalConfig.mongoUrl, {
134
- useNewUrlParser: true,
135
- useUnifiedTopology: true
136
- })
137
- .then(() => logger.logSuccess("MongoDB connected"))
138
- .catch(err => logger.logError(`MongoDB connection error: ${err.message}`));
139
- } else {
140
- logger.logInfo("Using existing mongoose connection");
141
- }
142
- // ------------------------------------------
143
-
144
- const auditService = finalConfig.features.auditLogs ? new AuditService() : null;
145
- const mfaService = new MFAService();
146
-
147
- // Pass shared services into AuthService config so it can log/audit correctly
148
- const authService = new AuthService({
149
- ...finalConfig,
150
- auditService,
151
- mfaService
152
- });
153
- const roleService = new RoleService();
154
- const passwordPolicy = new PasswordPolicy(finalConfig.passwordPolicy);
155
- const { rateLimiter, limiterFor, verifyToken, checkRole, checkOwnershipOrAdmin } = createAuthMiddleware(finalConfig);
156
-
157
- if (finalConfig.social && finalConfig.social.google) {
158
- passport.use(new GoogleStrategy(finalConfig.social.google,
159
- async (accessToken, refreshToken, profile, done) => {
160
- try {
161
- const user = await authService.handleSocialLogin('google', profile);
162
- done(null, user);
163
- } catch (err) {
164
- done(err);
165
- }
166
- }
167
- ));
168
- }
169
-
170
- return function (app) {
171
- const safeAudit = async (entry) => {
172
- if (!auditService) return;
173
- try {
174
- await auditService.logActivity(entry);
175
- } catch (err) {
176
- logger.logWarn(`Audit logging failed: ${err.message}`);
177
- }
178
- };
179
-
180
- app.use(cookieParser());
181
-
182
- if (finalConfig.enableCSRF) {
183
- const csrfProtection = csrf({ cookie: true });
184
- app.use(csrfProtection);
185
-
186
- app.get('/csrf-token', (req, res) => {
187
- res.json({ csrfToken: req.csrfToken() });
188
- });
189
- }
190
-
191
- app.use(passport.initialize());
192
- app.use(rateLimiter);
193
- app.use(helmet({
194
- contentSecurityPolicy: false,
195
- crossOriginEmbedderPolicy: false,
196
- }));
197
-
198
- app.authService = authService;
199
- app.roleService = roleService;
200
- app.auditService = auditService;
201
- app.mfaService = mfaService;
202
- app.passwordPolicy = passwordPolicy;
203
- app.authLogger = logger;
204
-
205
- // EMAIL VERIFICATION
206
- app.get(
207
- finalConfig.routes.verify,
208
- async (req, res, next) => {
209
- try {
210
- const token = req.query.token;
211
- const user = await authService.verifyEmail(token);
212
-
213
- res.json({
214
- success: true,
215
- message: 'Email verified successfully',
216
- userId: user._id
217
- });
218
- } catch (err) {
219
- next(err);
220
- }
221
- }
222
- );
223
-
224
- // REGISTER
225
- app.post(
226
- finalConfig.routes.register,
227
- limiterFor('register'),
228
- validators.registerValidator,
229
- validators.handleValidation,
230
- async (req, res, next) => {
231
- try {
232
- const { isValid, errors } = passwordPolicy.validate(req.body.password, {
233
- email: req.body.email,
234
- name: req.body.name
235
- });
236
-
237
- if (!isValid) {
238
- return res.status(400).json({ success: false, error: 'Password policy violation', details: errors });
239
- }
240
-
241
- const user = await authService.register(req.body);
242
- await safeAudit({
243
- userId: user._id,
244
- action: 'register',
245
- ipAddress: req.ip,
246
- userAgent: req.headers['user-agent'],
247
- status: 'success'
248
- });
249
-
250
- logger.logSuccess('User registered');
251
- res.status(201).json({ success: true, message: 'Registration successful' });
252
- } catch (err) {
253
- logger.logError(`Register failed: ${err.message}`);
254
- next(err);
255
- }
256
- }
257
- );
258
-
259
- // LOGIN
260
- app.post(
261
- finalConfig.routes.login,
262
- limiterFor('login'),
263
- validators.loginValidator,
264
- validators.handleValidation,
265
- async (req, res, next) => {
266
- try {
267
- const deviceInfo = { userAgent: req.headers['user-agent'], ip: req.ip };
268
- const result = await authService.login(req.body.email, req.body.password, deviceInfo);
269
-
270
- await safeAudit({
271
- userId: result.user?._id,
272
- action: 'login',
273
- ipAddress: req.ip,
274
- userAgent: req.headers['user-agent'],
275
- status: 'success'
276
- });
277
-
278
- logger.logSuccess('Login successful');
279
- res.json({ success: true, ...result });
280
- } catch (err) {
281
- await safeAudit({
282
- action: 'login',
283
- ipAddress: req.ip,
284
- userAgent: req.headers['user-agent'],
285
- status: 'failure',
286
- details: { error: err.message }
287
- });
288
- logger.logError(`Login failed: ${err.message}`);
289
- next(err);
290
- }
291
- }
292
- );
293
-
294
- // 2FA
295
- if (finalConfig.features.twoFactor !== false) {
296
- app.post(
297
- finalConfig.routes.twoFactor,
298
- limiterFor('2fa'),
299
- validators.twoFAValidator,
300
- validators.handleValidation,
301
- async (req, res, next) => {
302
- try {
303
- const result = await authService.verify2FA(req.body.userId, req.body.code);
304
- await safeAudit({ userId: result.user._id, action: '2fa_verify', status: 'success' });
305
- logger.logSuccess('2FA verified');
306
- res.json({ success: true, ...result });
307
- } catch (err) {
308
- logger.logError(`2FA failed: ${err.message}`);
309
- next(err);
310
- }
311
- }
312
- );
313
- }
314
-
315
- // FORGOT PASSWORD
316
- app.post(
317
- finalConfig.routes.forgotPassword,
318
- limiterFor('forgotPassword'),
319
- validators.forgotPasswordValidator,
320
- validators.handleValidation,
321
- async (req, res, next) => {
322
- try {
323
- await authService.forgotPassword(req.body.email);
324
- logger.logInfo('Password reset email triggered');
325
- res.json({ success: true, message: 'Password reset email sent if user exists' });
326
- } catch (err) {
327
- logger.logError(`Forgot password failed: ${err.message}`);
328
- next(err);
329
- }
330
- }
331
- );
332
-
333
- // RESET PASSWORD
334
- app.post(
335
- finalConfig.routes.resetPassword,
336
- validators.resetPasswordValidator,
337
- validators.handleValidation,
338
- async (req, res, next) => {
339
- try {
340
- await authService.resetPassword(req.body.token, req.body.newPassword);
341
- await safeAudit({ action: 'password_reset', status: 'success' });
342
- logger.logSuccess('Password reset completed');
343
- res.json({ success: true, message: 'Password reset successful' });
344
- } catch (err) {
345
- logger.logError(`Reset password failed: ${err.message}`);
346
- next(err);
347
- }
348
- }
349
- );
350
-
351
- // REFRESH TOKEN
352
- app.post(
353
- finalConfig.routes.refreshToken,
354
- validators.refreshTokenValidator,
355
- validators.handleValidation,
356
- async (req, res, next) => {
357
- try {
358
- const result = await authService.refreshToken(req.body.refreshToken);
359
- logger.logSuccess('Token refreshed');
360
- res.json({ success: true, ...result });
361
- } catch (err) {
362
- logger.logError(`Refresh token failed: ${err.message}`);
363
- next(err);
364
- }
365
- }
366
- );
367
-
368
- // UPDATE PROFILE
369
- app.put(
370
- finalConfig.routes.profile,
371
- verifyToken,
372
- checkOwnershipOrAdmin((req) => req.user.id),
373
- validators.profileUpdateValidator,
374
- validators.handleValidation,
375
- async (req, res, next) => {
376
- try {
377
- const user = await authService.updateProfile(req.user.id, req.body);
378
- await safeAudit({ userId: user._id, action: 'profile_update', status: 'success' });
379
- logger.logSuccess('Profile updated');
380
- res.json({ success: true, user });
381
- } catch (err) {
382
- logger.logError(`Profile update failed: ${err.message}`);
383
- next(err);
384
- }
385
- }
386
- );
387
-
388
- // CREATE ROLE (ADMIN)
389
- app.post('/roles', verifyToken, checkRole(['admin']), async (req, res, next) => {
390
- try {
391
- const role = await roleService.createRole(req.body);
392
- res.status(201).json({ success: true, role });
393
- } catch (err) {
394
- next(err);
395
- }
396
- });
397
-
398
- // GOOGLE OAUTH
399
- if (finalConfig.social && finalConfig.social.google) {
400
- app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
401
-
402
- app.get('/auth/google/callback',
403
- passport.authenticate('google', { session: false }),
404
- async (req, res, next) => {
405
- try {
406
- const tokens = await authService.generateTokens(req.user);
407
- await safeAudit({
408
- userId: req.user._id,
409
- action: 'social_login',
410
- status: 'success',
411
- details: { provider: 'google' }
412
- });
413
- logger.logSuccess('Google OAuth success');
414
- res.json({ success: true, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, user: req.user });
415
- } catch (err) {
416
- logger.logError(`Google OAuth failed: ${err.message}`);
417
- next(err);
418
- }
419
- });
420
- }
421
-
422
- // ERROR HANDLER
423
- app.use((err, req, res, next) => {
424
- console.error(err && err.stack ? err.stack : err);
425
- const status = err.status || (err.name === 'ValidationError' ? 400 : 500);
426
-
427
- let message;
428
- if (status >= 400 && status < 500) {
429
- message = err.message || 'Bad request';
430
- } else {
431
- message = process.env.NODE_ENV === 'development'
432
- ? (err.message || 'Internal Server Error')
433
- : 'Internal Server Error';
434
- }
435
-
436
- logger.logError(`Error ${status}: ${message}`);
437
- res.status(status).json({ success: false, error: message });
438
- });
439
- };
440
- }
441
-
442
- module.exports = { auth, ApiError, runAdaptiveTests, inferFeatureFlags, testCatalog };
1
+ const passport = require('passport');
2
+ const cookieParser = require('cookie-parser');
3
+ const csrf = require('csurf');
4
+ const helmet = require("helmet");
5
+ const mongoose = require("mongoose");
6
+ const jwt = require('jsonwebtoken');
7
+
8
+ const GoogleStrategy = require('passport-google-oauth20').Strategy;
9
+ const AuthService = require('./services/auth.service');
10
+ const RoleService = require('./services/role.service');
11
+ const AuditService = require('./services/audit.service');
12
+ const SessionService = require('./services/session.service');
13
+ const WebhookService = require('./services/webhook.service');
14
+ const PasswordlessService = require('./services/passwordless.service');
15
+ const MFAService = require('./utils/mfa');
16
+ const PasswordPolicy = require('./utils/password-policy');
17
+ const createAuthMiddleware = require('./middlewares/auth.middleware');
18
+ const corsMiddleware = require('./middlewares/cors.middleware');
19
+ const validators = require('./middlewares/validation.middleware');
20
+ const { createLogger } = require('./utils/logger');
21
+ const ApiError = require('./utils/api-error');
22
+ const { getPasswordStrength } = require('./utils/password-strength');
23
+ const { runAdaptiveTests, inferFeatureFlags, testCatalog } = require('./testing/adaptive-runner');
24
+
25
+
26
+ const defaultConfig = {
27
+ strategy: 'jwt',
28
+ fields: ['email', 'password'],
29
+ routes: {
30
+ register: '/register',
31
+ login: '/login',
32
+ verify: '/verify',
33
+ forgotPassword: '/forgot-password',
34
+ resetPassword: '/reset-password',
35
+ refreshToken: '/refresh-token',
36
+ profile: '/profile',
37
+ twoFactor: '/2fa',
38
+ roles: '/roles',
39
+ permissions: '/permissions',
40
+ magicLink: '/auth/magic-link',
41
+ magicLinkVerify: '/auth/magic-link/verify'
42
+ },
43
+ mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/forge-auth',
44
+ maxLoginAttempts: 5,
45
+ emailVerification: true,
46
+ loginAlerts: true,
47
+ passwordPolicy: {
48
+ minLength: 8,
49
+ requireUppercase: true,
50
+ requireNumbers: true,
51
+ requireSpecialChars: true,
52
+ expiryDays: 90
53
+ },
54
+ smtp: null,
55
+ social: {
56
+ google: null,
57
+ facebook: null,
58
+ github: null
59
+ },
60
+ features: {
61
+ emailVerification: true,
62
+ loginAlerts: true,
63
+ securityAttempts: true,
64
+ rateLimit: true,
65
+ auditLogs: true,
66
+ twoFactor: true,
67
+ csrf: false,
68
+ passwordless: false,
69
+ webhooks: false
70
+ },
71
+ env: {
72
+ JWT_SECRET: process.env.JWT_SECRET || 'crafta-auth-dev-secret'
73
+ },
74
+ accessTokenExpiry: '1h',
75
+ refreshTokenDays: 7,
76
+ cors: {
77
+ origin: '*',
78
+ credentials: true
79
+ },
80
+ webhooks: null,
81
+ session: {
82
+ maxSessions: 5,
83
+ sessionExpiryMs: 30 * 24 * 60 * 60 * 1000,
84
+ warningThresholdMs: 5 * 60 * 1000
85
+ }
86
+ };
87
+
88
+ function auth(config = {}) {
89
+ const finalConfig = {
90
+ ...defaultConfig,
91
+ ...config,
92
+ routes: { ...defaultConfig.routes, ...(config.routes || {}) },
93
+ passwordPolicy: { ...defaultConfig.passwordPolicy, ...(config.passwordPolicy || {}) },
94
+ social: { ...defaultConfig.social, ...(config.social || {}) },
95
+ limits: { ...(config.limits || {}) },
96
+ features: { ...defaultConfig.features, ...(config.features || {}) },
97
+ cors: { ...defaultConfig.cors, ...(config.cors || {}) },
98
+ session: { ...defaultConfig.session, ...(config.session || {}) }
99
+ };
100
+
101
+ finalConfig.env = { ...(defaultConfig.env || {}), ...(config.env || process.env) };
102
+ const logger = createLogger(finalConfig.logging !== false);
103
+
104
+ const featureBools = {
105
+ emailVerification: config.emailVerification,
106
+ loginAlerts: config.loginAlerts,
107
+ csrf: config.enableCSRF
108
+ };
109
+
110
+ Object.keys(featureBools).forEach((key) => {
111
+ if (typeof featureBools[key] === 'boolean') {
112
+ finalConfig.features[key] = featureBools[key];
113
+ }
114
+ });
115
+
116
+ finalConfig.emailVerification = finalConfig.features.emailVerification;
117
+ finalConfig.loginAlerts = finalConfig.features.loginAlerts;
118
+ finalConfig.enableCSRF = finalConfig.features.csrf;
119
+
120
+ if (finalConfig.features.securityAttempts === false) {
121
+ finalConfig.loginAlerts = false;
122
+ finalConfig.features.loginAlerts = false;
123
+ }
124
+
125
+ if (finalConfig.env?.JWT_SECRET === 'crafta-auth-dev-secret') {
126
+ logger.logWarn('JWT_SECRET not provided; using development default secret. Set env.JWT_SECRET in production.');
127
+ }
128
+
129
+ const smtpEnabled = !!(
130
+ finalConfig.smtp &&
131
+ finalConfig.smtp.host &&
132
+ finalConfig.smtp.port &&
133
+ finalConfig.smtp.auth &&
134
+ finalConfig.smtp.auth.user &&
135
+ finalConfig.smtp.auth.pass &&
136
+ finalConfig.smtp.from
137
+ );
138
+
139
+ if (!smtpEnabled && finalConfig.features.emailVerification) {
140
+ logger.logWarn('SMTP not configured; emailVerification auto-disabled.');
141
+ finalConfig.features.emailVerification = false;
142
+ finalConfig.emailVerification = false;
143
+ }
144
+
145
+ if (!smtpEnabled && finalConfig.features.loginAlerts) {
146
+ logger.logWarn('SMTP not configured; loginAlerts auto-disabled.');
147
+ finalConfig.features.loginAlerts = false;
148
+ finalConfig.loginAlerts = false;
149
+ }
150
+
151
+ // Passwordless requires SMTP for magic link delivery
152
+ if (finalConfig.features.passwordless && !smtpEnabled) {
153
+ logger.logWarn('SMTP not configured; passwordless (magic links) auto-disabled.');
154
+ finalConfig.features.passwordless = false;
155
+ }
156
+
157
+ // ------------------------------------------
158
+ // MongoDB Connection — support external connections
159
+ // ------------------------------------------
160
+ const connectDB = () => {
161
+ mongoose.connect(finalConfig.mongoUrl)
162
+ .then(() => logger.logSuccess("MongoDB connected"))
163
+ .catch(err => {
164
+ logger.logError(`MongoDB connection error: ${err.message}`);
165
+ setTimeout(connectDB, 5000);
166
+ });
167
+ };
168
+
169
+ if (finalConfig.mongooseConnection) {
170
+ // Allow host app to pass an existing mongoose connection
171
+ logger.logInfo("Using provided mongoose connection");
172
+ } else if (mongoose.connection.readyState === 0) {
173
+ connectDB();
174
+ mongoose.connection.on('disconnected', () => {
175
+ logger.logWarn("MongoDB disconnected, attempting to reconnect...");
176
+ connectDB();
177
+ });
178
+ } else {
179
+ logger.logInfo("Using existing mongoose connection");
180
+ }
181
+ // ------------------------------------------
182
+
183
+ const auditService = finalConfig.features.auditLogs ? new AuditService() : null;
184
+ const mfaService = new MFAService();
185
+ const sessionService = new SessionService(finalConfig);
186
+ const webhookService = new WebhookService(finalConfig);
187
+
188
+ // Pass shared services into AuthService config so it can log/audit correctly
189
+ const authService = new AuthService({
190
+ ...finalConfig,
191
+ auditService,
192
+ mfaService,
193
+ sessionService,
194
+ webhookService
195
+ });
196
+ const roleService = new RoleService();
197
+ const passwordPolicy = new PasswordPolicy(finalConfig.passwordPolicy);
198
+ const passwordlessService = finalConfig.features.passwordless
199
+ ? new PasswordlessService({ ...finalConfig, emailService: authService.emailService })
200
+ : null;
201
+
202
+ const { rateLimiter, limiterFor, verifyToken, checkRole, checkOwnershipOrAdmin } = createAuthMiddleware(finalConfig);
203
+
204
+ if (finalConfig.social && finalConfig.social.google) {
205
+ passport.use(new GoogleStrategy(finalConfig.social.google,
206
+ async (accessToken, refreshToken, profile, done) => {
207
+ try {
208
+ const user = await authService.handleSocialLogin('google', profile);
209
+ done(null, user);
210
+ } catch (err) {
211
+ done(err);
212
+ }
213
+ }
214
+ ));
215
+ }
216
+
217
+ return function (app) {
218
+ const safeAudit = async (entry) => {
219
+ if (!auditService) return;
220
+ try {
221
+ await auditService.logActivity(entry);
222
+ } catch (err) {
223
+ logger.logWarn(`Audit logging failed: ${err.message}`);
224
+ }
225
+ };
226
+
227
+ const safeWebhook = async (event, data) => {
228
+ if (!finalConfig.features.webhooks) return;
229
+ try {
230
+ await webhookService.emit(event, data);
231
+ } catch (err) {
232
+ logger.logWarn(`Webhook delivery failed: ${err.message}`);
233
+ }
234
+ };
235
+
236
+ // ------------------------------------------
237
+ // SESSION TIMEOUT WARNING MIDDLEWARE
238
+ // ------------------------------------------
239
+ const warningThresholdMs = finalConfig.session?.warningThresholdMs || 5 * 60 * 1000;
240
+ app.use((req, res, next) => {
241
+ const authHeader = req.headers.authorization;
242
+ if (authHeader && authHeader.startsWith('Bearer ')) {
243
+ const token = authHeader.split(' ')[1];
244
+ try {
245
+ const decoded = jwt.decode(token);
246
+ if (decoded && decoded.exp) {
247
+ const timeLeft = decoded.exp * 1000 - Date.now();
248
+ if (timeLeft > 0 && timeLeft < warningThresholdMs) {
249
+ res.setHeader('X-Session-Expires-In', Math.floor(timeLeft / 1000));
250
+ }
251
+ }
252
+ } catch (err) {
253
+ // Ignore decoding errors in middleware
254
+ }
255
+ }
256
+ next();
257
+ });
258
+
259
+ app.use(cookieParser());
260
+
261
+ // CORS — applied before routes
262
+ app.use(corsMiddleware(finalConfig));
263
+
264
+ if (finalConfig.enableCSRF) {
265
+ const csrfProtection = csrf({ cookie: true });
266
+ app.use(csrfProtection);
267
+
268
+ app.get('/csrf-token', (req, res) => {
269
+ res.json({ csrfToken: req.csrfToken() });
270
+ });
271
+ }
272
+
273
+ app.use(passport.initialize());
274
+ app.use(rateLimiter);
275
+ app.use(helmet({
276
+ contentSecurityPolicy: false,
277
+ crossOriginEmbedderPolicy: false,
278
+ }));
279
+
280
+ app.authService = authService;
281
+ app.roleService = roleService;
282
+ app.auditService = auditService;
283
+ app.mfaService = mfaService;
284
+ app.sessionService = sessionService;
285
+ app.webhookService = webhookService;
286
+ app.passwordlessService = passwordlessService;
287
+ app.passwordPolicy = passwordPolicy;
288
+ app.authLogger = logger;
289
+
290
+ // ------------------------------------------
291
+ // HEALTH CHECK
292
+ // ------------------------------------------
293
+ app.get('/auth/health', async (req, res) => {
294
+ const dbState = mongoose.connection.readyState;
295
+ const dbStatus = dbState === 1 ? 'connected' : dbState === 2 ? 'connecting' : 'disconnected';
296
+
297
+ // Get active session count
298
+ let activeSessions = 0;
299
+ if (mongoose.connection.readyState === 1) {
300
+ try {
301
+ activeSessions = await mongoose.connection.db.collection('users').aggregate([
302
+ { $match: { sessions: { $exists: true, $not: { $size: 0 } } } },
303
+ { $unwind: '$sessions' },
304
+ { $match: { 'sessions.createdAt': { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } } },
305
+ { $group: { _id: '$_id', count: { $sum: 1 } } },
306
+ { $group: { _id: null, total: { $sum: '$count' } } }
307
+ ]).toArray();
308
+ activeSessions = activeSessions[0]?.total || 0;
309
+ } catch (err) {
310
+ logger.logWarn(`Failed to count active sessions: ${err.message}`);
311
+ }
312
+ }
313
+
314
+ // Get memory usage
315
+ const memoryUsage = process.memoryUsage();
316
+
317
+ // Get uptime
318
+ const uptime = process.uptime();
319
+
320
+ res.json({
321
+ success: true,
322
+ status: 'ok',
323
+ db: dbStatus,
324
+ version: require('../package.json').version,
325
+ uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
326
+ activeSessions,
327
+ memoryUsage: {
328
+ rss: `${Math.round(memoryUsage.rss / 1024 / 1024)}MB`,
329
+ heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)}MB`,
330
+ heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)}MB`
331
+ },
332
+ features: {
333
+ emailVerification: finalConfig.features.emailVerification,
334
+ twoFactor: finalConfig.features.twoFactor,
335
+ passwordless: finalConfig.features.passwordless,
336
+ auditLogs: finalConfig.features.auditLogs,
337
+ webhooks: finalConfig.features.webhooks,
338
+ csrf: finalConfig.features.csrf,
339
+ rateLimit: finalConfig.features.rateLimit
340
+ }
341
+ });
342
+ });
343
+
344
+ // EMAIL VERIFICATION
345
+ app.get(
346
+ finalConfig.routes.verify,
347
+ async (req, res, next) => {
348
+ try {
349
+ const token = req.query.token;
350
+ const user = await authService.verifyEmail(token);
351
+
352
+ res.json({
353
+ success: true,
354
+ message: 'Email verified successfully',
355
+ userId: user._id
356
+ });
357
+ } catch (err) {
358
+ next(err);
359
+ }
360
+ }
361
+ );
362
+
363
+ // REGISTER
364
+ app.post(
365
+ finalConfig.routes.register,
366
+ limiterFor('register'),
367
+ validators.registerValidator,
368
+ validators.handleValidation,
369
+ async (req, res, next) => {
370
+ try {
371
+ const { isValid, errors } = passwordPolicy.validate(req.body.password, {
372
+ email: req.body.email,
373
+ name: req.body.name
374
+ });
375
+
376
+ if (!isValid) {
377
+ throw ApiError.passwordPolicy('Password policy violation', errors);
378
+ }
379
+
380
+ const user = await authService.register(req.body);
381
+ await safeAudit({
382
+ userId: user._id,
383
+ action: 'register',
384
+ ipAddress: req.ip,
385
+ userAgent: req.headers['user-agent'],
386
+ status: 'success'
387
+ });
388
+ await safeWebhook('user.registered', { userId: user._id, email: user.email });
389
+
390
+ logger.logSuccess('User registered');
391
+ res.status(201).json({ success: true, message: 'Registration successful' });
392
+ } catch (err) {
393
+ logger.logError(`Register failed: ${err.message}`);
394
+ next(err);
395
+ }
396
+ }
397
+ );
398
+
399
+ // LOGIN
400
+ app.post(
401
+ finalConfig.routes.login,
402
+ limiterFor('login'),
403
+ validators.loginValidator,
404
+ validators.handleValidation,
405
+ async (req, res, next) => {
406
+ try {
407
+ const deviceInfo = { userAgent: req.headers['user-agent'], ip: req.ip };
408
+ const result = await authService.login(req.body.email, req.body.password, deviceInfo);
409
+
410
+ await safeAudit({
411
+ userId: result.user?._id,
412
+ action: 'login',
413
+ ipAddress: req.ip,
414
+ userAgent: req.headers['user-agent'],
415
+ status: 'success'
416
+ });
417
+ await safeWebhook('user.login', { userId: result.user?._id });
418
+
419
+ logger.logSuccess('Login successful');
420
+ res.json({ success: true, ...result });
421
+ } catch (err) {
422
+ await safeAudit({
423
+ action: 'login',
424
+ ipAddress: req.ip,
425
+ userAgent: req.headers['user-agent'],
426
+ status: 'failure',
427
+ details: { error: err.message }
428
+ });
429
+ logger.logError(`Login failed: ${err.message}`);
430
+ next(err);
431
+ }
432
+ }
433
+ );
434
+
435
+ // 2FA
436
+ if (finalConfig.features.twoFactor !== false) {
437
+ app.post(
438
+ finalConfig.routes.twoFactor,
439
+ limiterFor('2fa'),
440
+ validators.twoFAValidator,
441
+ validators.handleValidation,
442
+ async (req, res, next) => {
443
+ try {
444
+ const result = await authService.verify2FA(req.body.userId, req.body.code);
445
+ await safeAudit({ userId: result.user._id, action: '2fa_verify', status: 'success' });
446
+ logger.logSuccess('2FA verified');
447
+ res.json({ success: true, ...result });
448
+ } catch (err) {
449
+ logger.logError(`2FA failed: ${err.message}`);
450
+ next(err);
451
+ }
452
+ }
453
+ );
454
+ }
455
+
456
+ // PASSWORDLESS / MAGIC LINK
457
+ if (finalConfig.features.passwordless && passwordlessService) {
458
+ app.post(
459
+ finalConfig.routes.magicLink,
460
+ limiterFor('login'),
461
+ async (req, res, next) => {
462
+ try {
463
+ const email = req.body.email;
464
+ if (!email) throw ApiError.validation('Email is required');
465
+ const baseUrl = finalConfig.baseUrl || `${req.protocol}://${req.get('host')}`;
466
+ await passwordlessService.requestMagicLink(email, baseUrl);
467
+ res.json({ success: true, message: 'Magic link sent if email exists' });
468
+ } catch (err) {
469
+ next(err);
470
+ }
471
+ }
472
+ );
473
+
474
+ app.get(
475
+ finalConfig.routes.magicLinkVerify,
476
+ async (req, res, next) => {
477
+ try {
478
+ const token = req.query.token;
479
+ if (!token) throw ApiError.invalidToken('Magic link token is required');
480
+ const user = await passwordlessService.verifyMagicLink(token);
481
+ const tokens = await authService.generateTokens(user);
482
+ await safeAudit({ userId: user._id, action: 'login', status: 'success', details: { method: 'magic_link' } });
483
+ await safeWebhook('user.login', { userId: user._id, method: 'magic_link' });
484
+ res.json({ success: true, ...tokens, user });
485
+ } catch (err) {
486
+ next(err);
487
+ }
488
+ }
489
+ );
490
+ }
491
+
492
+ // FORGOT PASSWORD
493
+ app.post(
494
+ finalConfig.routes.forgotPassword,
495
+ limiterFor('forgotPassword'),
496
+ validators.forgotPasswordValidator,
497
+ validators.handleValidation,
498
+ async (req, res, next) => {
499
+ try {
500
+ await authService.forgotPassword(req.body.email);
501
+ logger.logInfo('Password reset email triggered');
502
+ res.json({ success: true, message: 'Password reset email sent if user exists' });
503
+ } catch (err) {
504
+ logger.logError(`Forgot password failed: ${err.message}`);
505
+ next(err);
506
+ }
507
+ }
508
+ );
509
+
510
+ // RESET PASSWORD
511
+ app.post(
512
+ finalConfig.routes.resetPassword,
513
+ validators.resetPasswordValidator,
514
+ validators.handleValidation,
515
+ async (req, res, next) => {
516
+ try {
517
+ await authService.resetPassword(req.body.token, req.body.newPassword);
518
+ await safeAudit({ action: 'password_reset', status: 'success' });
519
+ await safeWebhook('user.password_changed', { token: req.body.token });
520
+ logger.logSuccess('Password reset completed');
521
+ res.json({ success: true, message: 'Password reset successful' });
522
+ } catch (err) {
523
+ logger.logError(`Reset password failed: ${err.message}`);
524
+ next(err);
525
+ }
526
+ }
527
+ );
528
+
529
+ // REFRESH TOKEN
530
+ app.post(
531
+ finalConfig.routes.refreshToken,
532
+ limiterFor('refreshToken'),
533
+ validators.refreshTokenValidator,
534
+ validators.handleValidation,
535
+ async (req, res, next) => {
536
+ try {
537
+ const result = await authService.refreshToken(req.body.refreshToken);
538
+ logger.logSuccess('Token refreshed');
539
+ res.json({ success: true, ...result });
540
+ } catch (err) {
541
+ logger.logError(`Refresh token failed: ${err.message}`);
542
+ next(err);
543
+ }
544
+ }
545
+ );
546
+
547
+ // LOGOUT
548
+ app.post(
549
+ '/logout',
550
+ verifyToken,
551
+ async (req, res, next) => {
552
+ try {
553
+ await authService.logout(req.user.id, req.body.sessionId, req.body.refreshToken);
554
+ await safeAudit({ userId: req.user.id, action: 'logout', status: 'success' });
555
+ await safeWebhook('user.logout', { userId: req.user.id });
556
+ res.json({ success: true, message: 'Logged out successfully' });
557
+ } catch (err) {
558
+ next(err);
559
+ }
560
+ }
561
+ );
562
+
563
+ // LOGOUT ALL DEVICES
564
+ app.post(
565
+ '/logout-all',
566
+ verifyToken,
567
+ async (req, res, next) => {
568
+ try {
569
+ const User = require('./models/user');
570
+ const user = await User.findById(req.user.id);
571
+ if (!user) throw ApiError.notFound('User not found');
572
+ await sessionService.revokeAllSessions(user);
573
+ await user.revokeAllRefreshTokens();
574
+ await safeAudit({ userId: req.user.id, action: 'logout', status: 'success', details: { all: true } });
575
+ res.json({ success: true, message: 'All sessions revoked' });
576
+ } catch (err) {
577
+ next(err);
578
+ }
579
+ }
580
+ );
581
+
582
+ // UPDATE PROFILE
583
+ app.put(
584
+ finalConfig.routes.profile,
585
+ verifyToken,
586
+ checkOwnershipOrAdmin((req) => req.user.id),
587
+ validators.profileUpdateValidator,
588
+ validators.handleValidation,
589
+ async (req, res, next) => {
590
+ try {
591
+ const user = await authService.updateProfile(req.user.id, req.body);
592
+ await safeAudit({ userId: user._id, action: 'profile_update', status: 'success' });
593
+ logger.logSuccess('Profile updated');
594
+ res.json({ success: true, user });
595
+ } catch (err) {
596
+ logger.logError(`Profile update failed: ${err.message}`);
597
+ next(err);
598
+ }
599
+ }
600
+ );
601
+
602
+ // CREATE ROLE (ADMIN)
603
+ app.post('/roles', verifyToken, checkRole(['admin']), async (req, res, next) => {
604
+ try {
605
+ const role = await roleService.createRole(req.body);
606
+ res.status(201).json({ success: true, role });
607
+ } catch (err) {
608
+ next(err);
609
+ }
610
+ });
611
+
612
+ // GOOGLE OAUTH
613
+ if (finalConfig.social && finalConfig.social.google) {
614
+ app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
615
+
616
+ app.get('/auth/google/callback',
617
+ passport.authenticate('google', { session: false }),
618
+ async (req, res, next) => {
619
+ try {
620
+ const tokens = await authService.generateTokens(req.user);
621
+ await safeAudit({
622
+ userId: req.user._id,
623
+ action: 'social_login',
624
+ status: 'success',
625
+ details: { provider: 'google' }
626
+ });
627
+ await safeWebhook('user.login', { userId: req.user._id, method: 'social', provider: 'google' });
628
+ logger.logSuccess('Google OAuth success');
629
+ res.json({ success: true, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, user: req.user });
630
+ } catch (err) {
631
+ logger.logError(`Google OAuth failed: ${err.message}`);
632
+ next(err);
633
+ }
634
+ });
635
+ }
636
+
637
+ // ------------------------------------------
638
+ // STANDARDIZED ERROR HANDLER
639
+ // ------------------------------------------
640
+ app.use((err, req, res, next) => {
641
+ // Normalize any error into our ApiError shape
642
+ const apiErr = ApiError.normalize(err);
643
+
644
+ // Log stack only in development
645
+ if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === undefined) {
646
+ logger.logError(`[${apiErr.code}] ${apiErr.message}`);
647
+ } else {
648
+ // In production, only log server errors with stack
649
+ if (apiErr.status >= 500) {
650
+ logger.logError(`[${apiErr.code}] ${apiErr.message}`);
651
+ }
652
+ }
653
+
654
+ const response = {
655
+ success: false,
656
+ code: apiErr.code,
657
+ error: apiErr.message
658
+ };
659
+
660
+ if (apiErr.details) {
661
+ response.details = apiErr.details;
662
+ }
663
+
664
+ res.status(apiErr.status).json(response);
665
+ });
666
+ };
667
+ }
668
+
669
+ module.exports = { auth, ApiError, getPasswordStrength, runAdaptiveTests, inferFeatureFlags, testCatalog };