@dax-crafta/auth 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dev-testing.md +48 -0
- package/package.json +65 -62
- package/src/index.d.ts +104 -9
- package/src/index.js +442 -336
- package/src/middlewares/auth.middleware.js +19 -15
- package/src/models/audit-log.js +18 -1
- package/src/models/user.js +20 -2
- package/src/services/auth.service.js +92 -11
- package/src/testing/adaptive-runner.js +74 -0
- package/src/utils/api-error.js +8 -0
- package/src/utils/email.js +21 -4
- package/src/utils/logger.js +38 -0
package/src/index.js
CHANGED
|
@@ -1,336 +1,442 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
res.json({
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
async (req, res, next) => {
|
|
302
|
-
try {
|
|
303
|
-
const
|
|
304
|
-
await
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 = { browser: 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 };
|