@feardread/fear 1.1.4 → 1.1.6
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/FEARServer.js +2 -2
- package/controllers/address.js +9 -0
- package/controllers/auth/index.js +262 -91
- package/controllers/order.js +0 -1
- package/controllers/payment.js +180 -20
- package/libs/db/index.js +5 -0
- package/libs/validator/index.js +2 -2
- package/models/address.js +37 -0
- package/models/order.js +249 -72
- package/models/payment.js +139 -0
- package/models/user.js +116 -49
- package/package.json +1 -1
- package/routes/address.js +15 -0
- package/routes/order.js +4 -2
- package/routes/payment.js +18 -0
package/FEARServer.js
CHANGED
|
@@ -71,9 +71,9 @@ const FearServer = (function () {
|
|
|
71
71
|
setupProcessHandlers() {
|
|
72
72
|
// Handle unhandled promise rejections
|
|
73
73
|
process.on("unhandledRejection", (reason, promise) => {
|
|
74
|
-
console.log('Unhandled Rejection at:', promise
|
|
74
|
+
console.log('Unhandled Rejection at:', promise);
|
|
75
75
|
this.fear.getLogger().error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
76
|
-
|
|
76
|
+
this.gracefulShutdown('unhandledRejection');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
// Handle uncaught exceptions
|
|
@@ -18,15 +18,8 @@ const response = {
|
|
|
18
18
|
* @param {string} message - Success message
|
|
19
19
|
*/
|
|
20
20
|
success: (res, user, token, statusCode = 200, message = "Authentication successful") => {
|
|
21
|
-
//
|
|
22
|
-
const userResponse =
|
|
23
|
-
_id: user._id,
|
|
24
|
-
email: user.email,
|
|
25
|
-
name: user.name,
|
|
26
|
-
role: user.role,
|
|
27
|
-
createdAt: user.createdAt,
|
|
28
|
-
updatedAt: user.updatedAt
|
|
29
|
-
};
|
|
21
|
+
// Use the model's toJSON method which automatically removes sensitive data
|
|
22
|
+
const userResponse = user.toJSON();
|
|
30
23
|
|
|
31
24
|
return res
|
|
32
25
|
.status(statusCode)
|
|
@@ -64,34 +57,72 @@ const response = {
|
|
|
64
57
|
* @description Validates user credentials (email/password) and returns JWT token for authenticated user
|
|
65
58
|
* @tags authentication
|
|
66
59
|
*/
|
|
67
|
-
exports.login =
|
|
60
|
+
exports.login = (req, res) => {
|
|
68
61
|
const { email, password } = req.body;
|
|
69
62
|
|
|
70
63
|
// Validate input
|
|
71
64
|
const validation = validator.input.login({ email, password });
|
|
72
65
|
if (!validation.isValid) {
|
|
73
|
-
return response.
|
|
66
|
+
return response.error(res, 400, validation.message);
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
logger.info('Authentication attempt for:', email);
|
|
70
|
+
|
|
71
|
+
// Find user and include password field
|
|
72
|
+
User.findOne({
|
|
73
|
+
email: email.toLowerCase(),
|
|
74
|
+
status: { $ne: 'deleted' }
|
|
75
|
+
})
|
|
76
|
+
.select('+password')
|
|
77
|
+
.then(user => {
|
|
78
|
+
if (!user) {
|
|
79
|
+
return response.error(res, 401, "Invalid credentials");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if account is locked
|
|
83
|
+
if (user.isLocked()) {
|
|
84
|
+
return response.error(
|
|
85
|
+
res,
|
|
86
|
+
423,
|
|
87
|
+
"Account temporarily locked due to too many failed login attempts. Please try again later."
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if account is suspended or inactive
|
|
92
|
+
if (user.status === 'suspended') {
|
|
93
|
+
return response.error(res, 403, "Your account has been suspended. Please contact support.");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (user.status === 'inactive') {
|
|
97
|
+
return response.error(res, 403, "Your account is inactive. Please contact support to reactivate.");
|
|
98
|
+
}
|
|
80
99
|
|
|
81
100
|
// Verify password
|
|
82
|
-
user.
|
|
83
|
-
.then(
|
|
101
|
+
return user.comparePassword(password)
|
|
102
|
+
.then(isPasswordValid => {
|
|
84
103
|
if (!isPasswordValid) {
|
|
85
|
-
return
|
|
104
|
+
return user.incLoginAttempts()
|
|
105
|
+
.then(() => {
|
|
106
|
+
return response.error(res, 401, "Invalid credentials");
|
|
107
|
+
});
|
|
86
108
|
}
|
|
87
109
|
|
|
88
|
-
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
110
|
+
// Reset login attempts and update last login
|
|
111
|
+
return user.resetLoginAttempts()
|
|
112
|
+
.then(() => {
|
|
113
|
+
return user.updateOne({
|
|
114
|
+
lastLoginAt: new Date(),
|
|
115
|
+
lastLoginIP: req.ip || req.connection.remoteAddress
|
|
116
|
+
});
|
|
117
|
+
})
|
|
118
|
+
.then(() => {
|
|
119
|
+
// Generate token
|
|
120
|
+
const token = TokenService.generateToken(user);
|
|
121
|
+
return response.success(res, user, token, 200, "Login successful");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
93
124
|
})
|
|
94
|
-
.catch(
|
|
125
|
+
.catch(error => {
|
|
95
126
|
logger.error('Login error:', error);
|
|
96
127
|
return response.error(res, 500, "Authentication failed", error.message);
|
|
97
128
|
});
|
|
@@ -103,50 +134,77 @@ exports.login = async (req, res) => {
|
|
|
103
134
|
* @description Creates a new user account with provided information and returns JWT token
|
|
104
135
|
* @tags authentication
|
|
105
136
|
*/
|
|
106
|
-
exports.register =
|
|
107
|
-
const {
|
|
137
|
+
exports.register = (req, res) => {
|
|
138
|
+
const {
|
|
139
|
+
email,
|
|
140
|
+
password,
|
|
141
|
+
firstName,
|
|
142
|
+
lastName,
|
|
143
|
+
displayName,
|
|
144
|
+
phoneNumber,
|
|
145
|
+
dateOfBirth,
|
|
146
|
+
...otherFields
|
|
147
|
+
} = req.body;
|
|
108
148
|
|
|
109
149
|
// Validate input
|
|
110
150
|
const validation = validator.input.register({
|
|
111
|
-
email,
|
|
151
|
+
email,
|
|
152
|
+
password,
|
|
153
|
+
firstName,
|
|
154
|
+
lastName
|
|
112
155
|
});
|
|
113
156
|
|
|
114
157
|
if (!validation.isValid) {
|
|
115
158
|
return response.error(res, 400, validation.message);
|
|
116
159
|
}
|
|
117
160
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
161
|
+
// Check if user already exists
|
|
162
|
+
User.findOne({ email: email.toLowerCase() })
|
|
163
|
+
.then(existingUser => {
|
|
164
|
+
if (existingUser) {
|
|
165
|
+
return response.error(res, 409, "User with this email already exists");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Create new user with nested profile structure
|
|
169
|
+
const userData = {
|
|
170
|
+
email: email.toLowerCase(),
|
|
171
|
+
password,
|
|
172
|
+
firstName: firstName.trim(),
|
|
173
|
+
lastName: lastName.trim(),
|
|
174
|
+
displayName: displayName || `${firstName} ${lastName}`,
|
|
175
|
+
mobile: phoneNumber || undefined,
|
|
176
|
+
dateOfBirth: dateOfBirth || undefined,
|
|
177
|
+
role: otherFields.role || 'user',
|
|
178
|
+
status: 'active'
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return User.create(userData);
|
|
182
|
+
})
|
|
183
|
+
.then(user => {
|
|
184
|
+
if (!user) return; // Already handled in previous then
|
|
124
185
|
|
|
125
|
-
|
|
126
|
-
const userData = {
|
|
127
|
-
...otherFields,
|
|
128
|
-
email: email.toLowerCase(),
|
|
129
|
-
name: validation.name,
|
|
130
|
-
password,
|
|
131
|
-
role: otherFields.role || 'user' // Default role
|
|
132
|
-
};
|
|
186
|
+
logger.info('New user registered:', user.email);
|
|
133
187
|
|
|
134
|
-
|
|
188
|
+
// Generate token and send response
|
|
189
|
+
const token = TokenService.generateToken(user);
|
|
190
|
+
return response.success(res, user, token, 201, "Registration successful");
|
|
191
|
+
})
|
|
192
|
+
.catch(error => {
|
|
193
|
+
logger.error('Registration error:', error);
|
|
135
194
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
195
|
+
// Handle duplicate key error (in case of race condition)
|
|
196
|
+
if (error.code === 11000) {
|
|
197
|
+
return response.error(res, 409, "User with this email already exists");
|
|
198
|
+
}
|
|
139
199
|
|
|
140
|
-
|
|
141
|
-
|
|
200
|
+
// Handle validation errors
|
|
201
|
+
if (error.name === 'ValidationError') {
|
|
202
|
+
const messages = Object.values(error.errors).map(err => err.message);
|
|
203
|
+
return response.error(res, 400, messages.join(', '));
|
|
204
|
+
}
|
|
142
205
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return response.error(res, 409, "User with this email already exists");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return response.error(res, 500, "Registration failed", error.message);
|
|
149
|
-
}
|
|
206
|
+
return response.error(res, 500, "Registration failed", error.message);
|
|
207
|
+
});
|
|
150
208
|
};
|
|
151
209
|
|
|
152
210
|
/**
|
|
@@ -175,18 +233,12 @@ exports.logout = async (req, res) => {
|
|
|
175
233
|
* @description Returns the current user's profile information
|
|
176
234
|
* @tags authentication
|
|
177
235
|
*/
|
|
178
|
-
exports.getCurrentUser =
|
|
236
|
+
exports.getCurrentUser = (req, res) => {
|
|
179
237
|
// req.user is set by isAuthorized middleware
|
|
180
238
|
const user = req.user;
|
|
181
239
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
email: user.email,
|
|
185
|
-
name: user.name,
|
|
186
|
-
role: user.role,
|
|
187
|
-
createdAt: user.createdAt,
|
|
188
|
-
updatedAt: user.updatedAt
|
|
189
|
-
};
|
|
240
|
+
// Use toJSON to automatically remove sensitive fields
|
|
241
|
+
const userResponse = user.toJSON();
|
|
190
242
|
|
|
191
243
|
return res.status(200).json({
|
|
192
244
|
success: true,
|
|
@@ -200,20 +252,112 @@ exports.getCurrentUser = async (req, res) => {
|
|
|
200
252
|
* @description Generates a new JWT token for authenticated user
|
|
201
253
|
* @tags authentication
|
|
202
254
|
*/
|
|
203
|
-
exports.refreshToken =
|
|
255
|
+
exports.refreshToken = (req, res) => {
|
|
204
256
|
const user = req.user; // Set by isAuthorized middleware
|
|
205
257
|
|
|
206
258
|
const newToken = TokenService.generateToken(user);
|
|
207
259
|
return response.success(res, user, newToken, 200, "Token refreshed successfully");
|
|
208
260
|
};
|
|
209
261
|
|
|
262
|
+
/**
|
|
263
|
+
* PUT /fear/api/auth/update-profile
|
|
264
|
+
* @summary Update user profile information
|
|
265
|
+
* @description Updates the authenticated user's profile data
|
|
266
|
+
* @tags authentication
|
|
267
|
+
*/
|
|
268
|
+
exports.updateProfile = (req, res) => {
|
|
269
|
+
const { firstName, lastName, displayName, phoneNumber, bio, dateOfBirth, avatar } = req.body;
|
|
270
|
+
|
|
271
|
+
User.findById(req.user._id)
|
|
272
|
+
.then(user => {
|
|
273
|
+
if (!user) {
|
|
274
|
+
return response.error(res, 404, "User not found");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Update profile fields if provided
|
|
278
|
+
if (firstName) user.firstName = firstName.trim();
|
|
279
|
+
if (lastName) user.lastName = lastName.trim();
|
|
280
|
+
if (displayName !== undefined) user.displayName = displayName.trim();
|
|
281
|
+
if (phoneNumber !== undefined) user.phoneNumber = phoneNumber;
|
|
282
|
+
if (bio !== undefined) user.bio = bio;
|
|
283
|
+
if (dateOfBirth !== undefined) user.dateOfBirth = dateOfBirth;
|
|
284
|
+
if (avatar !== undefined) user.avatar = avatar;
|
|
285
|
+
|
|
286
|
+
return user.save();
|
|
287
|
+
})
|
|
288
|
+
.then(user => {
|
|
289
|
+
if (!user) return; // Already handled
|
|
290
|
+
|
|
291
|
+
return res.status(200).json({
|
|
292
|
+
success: true,
|
|
293
|
+
message: "Profile updated successfully",
|
|
294
|
+
data: { user: user.toJSON() }
|
|
295
|
+
});
|
|
296
|
+
})
|
|
297
|
+
.catch(error => {
|
|
298
|
+
logger.error('Profile update error:', error);
|
|
299
|
+
|
|
300
|
+
if (error.name === 'ValidationError') {
|
|
301
|
+
const messages = Object.values(error.errors).map(err => err.message);
|
|
302
|
+
return response.error(res, 400, messages.join(', '));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return response.error(res, 500, "Profile update failed", error.message);
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* PUT /fear/api/auth/update-preferences
|
|
311
|
+
* @summary Update user preferences
|
|
312
|
+
* @description Updates the authenticated user's preferences (language, timezone, notifications, theme)
|
|
313
|
+
* @tags authentication
|
|
314
|
+
*/
|
|
315
|
+
exports.updatePreferences = (req, res) => {
|
|
316
|
+
const { language, timezone, notifications, theme } = req.body;
|
|
317
|
+
|
|
318
|
+
User.findById(req.user._id)
|
|
319
|
+
.then(user => {
|
|
320
|
+
if (!user) {
|
|
321
|
+
return response.error(res, 404, "User not found");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Update preferences
|
|
325
|
+
if (language) user.preferences.language = language;
|
|
326
|
+
if (timezone) user.preferences.timezone = timezone;
|
|
327
|
+
if (theme) user.preferences.theme = theme;
|
|
328
|
+
if (notifications) {
|
|
329
|
+
user.preferences.notifications = {
|
|
330
|
+
...user.preferences.notifications,
|
|
331
|
+
...notifications
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return user.save();
|
|
336
|
+
})
|
|
337
|
+
.then(user => {
|
|
338
|
+
if (!user) return; // Already handled
|
|
339
|
+
|
|
340
|
+
return res.status(200).json({
|
|
341
|
+
success: true,
|
|
342
|
+
message: "Preferences updated successfully",
|
|
343
|
+
data: {
|
|
344
|
+
preferences: user.preferences
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
})
|
|
348
|
+
.catch(error => {
|
|
349
|
+
logger.error('Preferences update error:', error);
|
|
350
|
+
return response.error(res, 500, "Preferences update failed", error.message);
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
|
|
210
354
|
/**
|
|
211
355
|
* Middleware: Verify JWT token and authenticate user
|
|
212
356
|
* @summary Checks if user has valid JWT token in cookies or Authorization header
|
|
213
357
|
* @description Validates JWT token and attaches user object to request
|
|
214
358
|
* @tags middleware, authentication
|
|
215
359
|
*/
|
|
216
|
-
exports.isAuthorized =
|
|
360
|
+
exports.isAuthorized = (req, res, next) => {
|
|
217
361
|
let token;
|
|
218
362
|
|
|
219
363
|
// Check for token in cookies first, then Authorization header
|
|
@@ -232,23 +376,37 @@ exports.isAuthorized = async (req, res, next) => {
|
|
|
232
376
|
const decodedToken = TokenService.verifyToken(token);
|
|
233
377
|
|
|
234
378
|
// Get user from database
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
379
|
+
User.findById(decodedToken.id)
|
|
380
|
+
.then(user => {
|
|
381
|
+
if (!user) {
|
|
382
|
+
return response.error(res, 401, "Invalid token. User not found.");
|
|
383
|
+
}
|
|
239
384
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
385
|
+
// Check user status
|
|
386
|
+
if (user.status === 'deleted') {
|
|
387
|
+
return response.error(res, 401, "Account has been deleted.");
|
|
388
|
+
}
|
|
244
389
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
390
|
+
if (user.status === 'suspended') {
|
|
391
|
+
return response.error(res, 403, "Account has been suspended.");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (user.status === 'inactive') {
|
|
395
|
+
return response.error(res, 403, "Account is inactive.");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Attach user to request
|
|
399
|
+
req.user = user;
|
|
400
|
+
req.token = token;
|
|
401
|
+
next();
|
|
402
|
+
})
|
|
403
|
+
.catch(error => {
|
|
404
|
+
logger.error('Authorization error:', error);
|
|
405
|
+
return response.error(res, 500, "Authentication failed", error.message);
|
|
406
|
+
});
|
|
249
407
|
|
|
250
408
|
} catch (error) {
|
|
251
|
-
|
|
409
|
+
logger.error('Authorization error:', error);
|
|
252
410
|
|
|
253
411
|
if (error.name === 'JsonWebTokenError') {
|
|
254
412
|
return response.error(res, 401, "Invalid token.");
|
|
@@ -268,7 +426,7 @@ exports.isAuthorized = async (req, res, next) => {
|
|
|
268
426
|
* @description Checks if req.user.role equals 'admin'. Must be used after isAuthorized middleware
|
|
269
427
|
* @tags middleware, authorization
|
|
270
428
|
*/
|
|
271
|
-
exports.isAdmin =
|
|
429
|
+
exports.isAdmin = (req, res, next) => {
|
|
272
430
|
if (!req.user) {
|
|
273
431
|
return response.error(res, 401, "Authentication required");
|
|
274
432
|
}
|
|
@@ -289,7 +447,7 @@ exports.isAdmin = async (req, res, next) => {
|
|
|
289
447
|
* @returns {function} Express middleware function
|
|
290
448
|
*/
|
|
291
449
|
exports.authorizeRoles = (...roles) => {
|
|
292
|
-
return
|
|
450
|
+
return (req, res, next) => {
|
|
293
451
|
if (!req.user) {
|
|
294
452
|
return response.error(res, 401, "Authentication required");
|
|
295
453
|
}
|
|
@@ -303,7 +461,7 @@ exports.authorizeRoles = (...roles) => {
|
|
|
303
461
|
}
|
|
304
462
|
|
|
305
463
|
next();
|
|
306
|
-
}
|
|
464
|
+
};
|
|
307
465
|
};
|
|
308
466
|
|
|
309
467
|
/**
|
|
@@ -312,7 +470,7 @@ exports.authorizeRoles = (...roles) => {
|
|
|
312
470
|
* @description Useful for routes that behave differently for authenticated vs anonymous users
|
|
313
471
|
* @tags middleware, authentication
|
|
314
472
|
*/
|
|
315
|
-
exports.optionalAuth =
|
|
473
|
+
exports.optionalAuth = (req, res, next) => {
|
|
316
474
|
let token;
|
|
317
475
|
|
|
318
476
|
if (req.cookies?.jwt) {
|
|
@@ -327,19 +485,32 @@ exports.optionalAuth = async (req, res, next) => {
|
|
|
327
485
|
|
|
328
486
|
try {
|
|
329
487
|
const decodedToken = TokenService.verifyToken(token);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
488
|
+
|
|
489
|
+
User.findById(decodedToken.id)
|
|
490
|
+
.then(user => {
|
|
491
|
+
if (user && user.status === 'active') {
|
|
492
|
+
req.user = user;
|
|
493
|
+
req.token = token;
|
|
494
|
+
}
|
|
495
|
+
next();
|
|
496
|
+
})
|
|
497
|
+
.catch(error => {
|
|
498
|
+
// Silently fail for optional auth
|
|
499
|
+
logger.debug('Optional auth failed:', error.message);
|
|
500
|
+
next();
|
|
501
|
+
});
|
|
336
502
|
} catch (error) {
|
|
337
503
|
// Silently fail for optional auth
|
|
338
|
-
|
|
504
|
+
logger.debug('Optional auth failed:', error.message);
|
|
505
|
+
next();
|
|
339
506
|
}
|
|
340
|
-
|
|
341
|
-
next();
|
|
342
507
|
};
|
|
343
508
|
|
|
344
509
|
// Export TokenService and other utilities for use in other modules
|
|
345
510
|
exports.AuthResponse = response;
|
|
511
|
+
|
|
512
|
+
module.exports = {
|
|
513
|
+
login: exports.login,
|
|
514
|
+
register: exports.register,
|
|
515
|
+
logout: exports.logout
|
|
516
|
+
}
|