@dax-crafta/auth 2.1.5 → 2.1.7

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