@feardread/fear 1.1.5 → 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 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, 'reason:', reason);
74
+ console.log('Unhandled Rejection at:', promise);
75
75
  this.fear.getLogger().error('Unhandled Rejection at:', promise, 'reason:', reason);
76
- //this.gracefulShutdown('unhandledRejection');
76
+ this.gracefulShutdown('unhandledRejection');
77
77
  });
78
78
 
79
79
  // Handle uncaught exceptions
@@ -0,0 +1,9 @@
1
+ const Address = require("../models/address");
2
+ const methods = require("./crud");
3
+
4
+ const crud = methods.crudController( Address );
5
+ for(prop in crud) {
6
+ if(crud.hasOwnProperty(prop)) {
7
+ module.exports[prop] = crud[prop];
8
+ }
9
+ }
@@ -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
- // Remove sensitive data from user object
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 = async (req, res) => {
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.sendError(res, 400, validation.message);
66
+ return response.error(res, 400, validation.message);
74
67
  }
75
68
 
76
- console.log('Authentication attempt for:', email);
77
- await User.findOne({ email }).select('+password')
78
- .then((user) => {
79
- if (!user) return response.error(res, 401, "Invalid credentials");
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.compare(password)
83
- .then((isPasswordValid) => {
101
+ return user.comparePassword(password)
102
+ .then(isPasswordValid => {
84
103
  if (!isPasswordValid) {
85
- return response.error(res, 401, "Invalid credentials");
104
+ return user.incLoginAttempts()
105
+ .then(() => {
106
+ return response.error(res, 401, "Invalid credentials");
107
+ });
86
108
  }
87
109
 
88
- const token = TokenService.generateToken(user);
89
- return response.success(res, user, token, 200, "Login successful");
90
- })
91
- .catch(err => response.error(res, 500, 'Server Error'))
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((error) => {
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 = async (req, res) => {
107
- const { email, firstname, lastname, name: providedName, password, ...otherFields } = req.body;
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, firstname, lastname, name: providedName, password
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
- try {
119
- // Check if user already exists
120
- const existingUser = await User.findOne({ email });
121
- if (existingUser) {
122
- return response.error(res, 409, "User with this email already exists");
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
- // Create new user
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
- const user = await User.create(userData);
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
- // Generate token and send response
137
- const token = TokenService.generateToken(user);
138
- return response.success(res, user, token, 201, "Registration successful");
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
- } catch (error) {
141
- console.error('Registration error:', error);
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
- // Handle duplicate key error (in case of race condition)
144
- if (error.code === 11000) {
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 = async (req, res) => {
236
+ exports.getCurrentUser = (req, res) => {
179
237
  // req.user is set by isAuthorized middleware
180
238
  const user = req.user;
181
239
 
182
- const userResponse = {
183
- _id: user._id,
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 = async (req, res) => {
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 = async (req, res, next) => {
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
- const user = await User.findById(decodedToken.id);
236
- if (!user) {
237
- return response.error(res, 401, "Invalid token. User not found.");
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
- // Check if user is still active (optional)
241
- if (user.isActive === false) {
242
- return response.error(res, 401, "Account has been deactivated.");
243
- }
385
+ // Check user status
386
+ if (user.status === 'deleted') {
387
+ return response.error(res, 401, "Account has been deleted.");
388
+ }
244
389
 
245
- // Attach user to request
246
- req.user = user;
247
- req.token = token;
248
- next();
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
- console.error('Authorization error:', error);
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 = async (req, res, next) => {
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 handler.async(async (req, res, next) => {
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 = async (req, res, next) => {
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
- const user = await User.findById(decodedToken.id);
331
-
332
- if (user && user.isActive !== false) {
333
- req.user = user;
334
- req.token = token;
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
- console.log('Optional auth failed:', error.message);
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
+ }
@@ -7,7 +7,6 @@ exports.getMyOrders = async (req, res) => {
7
7
  const orders = await Order.find({ user: _id })
8
8
  .populate("user")
9
9
  .populate("orderItems.product")
10
- .populate("orderItems.color");
11
10
  res.json({
12
11
  orders,
13
12
  });
package/libs/db/index.js CHANGED
@@ -41,6 +41,11 @@ class DatabaseManager {
41
41
  await this.close();
42
42
  process.exit(0);
43
43
  });
44
+
45
+ process.on('UnhandledRejection', async (promise, reson) => {
46
+ console.log('Unhandled Rejection at ', reason);
47
+ await this.close();
48
+ })
44
49
  }
45
50
 
46
51
  /**
@@ -52,7 +52,7 @@ exports.input = {
52
52
  * @returns {object} Validation result
53
53
  */
54
54
  register: (data) => {
55
- const { email, password, firstname, lastname, name } = data;
55
+ const { email, password, firstName, lastName, displayName } = data;
56
56
 
57
57
  if (!email) return { isValid: false, message: "Email is required" };
58
58
  if (!exports.input.email(email)) return { isValid: false, message: "Please provide a valid email address" };
@@ -60,7 +60,7 @@ exports.input = {
60
60
  const passwordValidation = exports.input.password(password);
61
61
  if (!passwordValidation.isValid) return passwordValidation;
62
62
 
63
- const fullName = firstname && lastname ? `${firstname} ${lastname}` : name;
63
+ const fullName = (displayName) ? displayName : `${firstName} ${lastName}`;
64
64
  if (!fullName) return { isValid: false, message: "Name is required" };
65
65
 
66
66
  return { isValid: true, name: fullName };
@@ -0,0 +1,37 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const addressSchema = new mongoose.Schema({
4
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true },
5
+ type: { type: String, enum: ['home', 'work', 'billing', 'shipping', 'other'], default: 'billing' },
6
+ isDefault: { type: Boolean, default: false },
7
+ name: { type: String, trim: true, maxlength: 50 },
8
+ line1: { type: String, required: true, trim: true },
9
+ line2: { type: String, trim: true },
10
+ city: { type: String, required: true, trim: true },
11
+ state: { type: String, required: true, trim: true },
12
+ postalCode: { type: String, required: true, trim: true },
13
+ country: { type: String, required: true, default: 'US', uppercase: true },
14
+ coordinates: { type: { type: String, enum: ['Point'], default: 'Point' },
15
+ coordinates: { type: [Number], default: [0, 0] }},
16
+ phoneNumber: { type: String, trim: true },
17
+ deliveryInstructions: { type: String, trim: true, maxlength: 500 }
18
+ }, {
19
+ timestamps: true
20
+ });
21
+
22
+ // Indexes
23
+ addressSchema.index({ userId: 1, isDefault: 1 });
24
+ addressSchema.index({ coordinates: '2dsphere' });
25
+
26
+ // Ensure only one default address per user
27
+ addressSchema.pre('save', async function(next) {
28
+ if (this.isDefault) {
29
+ await this.constructor.updateMany(
30
+ { userId: this.userId, _id: { $ne: this._id } },
31
+ { $set: { isDefault: false } }
32
+ );
33
+ }
34
+ next();
35
+ });
36
+
37
+ module.exports = mongoose.model('Address', addressSchema);
package/models/order.js CHANGED
@@ -1,93 +1,30 @@
1
1
  const mongoose = require("mongoose");
2
2
 
3
3
  const orderItemSchema = new mongoose.Schema({
4
- productId: {
5
- type: mongoose.Schema.Types.ObjectId,
6
- ref: "Product",
7
- required: true,
8
- },
9
- name: {
10
- type: String,
11
- required: true,
12
- },
13
- quantity: {
14
- type: Number,
15
- required: true,
16
- min: 1,
17
- },
18
- price: {
19
- type: Number,
20
- required: true,
21
- },
22
- discount: {
23
- type: Number,
24
- default: 0,
25
- },
26
- tax: {
27
- type: Number,
28
- default: 0,
29
- },
30
- subtotal: {
31
- type: Number,
32
- required: true,
33
- },
4
+ productId: { type: mongoose.Schema.Types.ObjectId, ref: "Product", required: true },
5
+ title: { type: String, required: true, },
6
+ quantity: { type: Number, required: true, min: 1, },
7
+ price: { type: Number, required: true, },
8
+ discount: { type: Number, default: 0, },
9
+ tax: { type: Number, default: 0, },
10
+ subtotal: { type: Number, required: true, },
34
11
  image: String,
35
12
  sku: String,
36
- attributes: {
37
- type: Map,
38
- of: String, // For size, color, etc.
39
- },
13
+ attributes: { type: Map, of: String, },
40
14
  });
41
15
 
42
16
  const orderSchema = new mongoose.Schema(
43
17
  {
44
- orderNumber: {
45
- type: String,
46
- unique: true,
47
- required: true,
48
- },
49
- userId: {
50
- type: mongoose.Schema.Types.ObjectId,
51
- ref: "User",
52
- required: true,
53
- index: true,
54
- },
55
- // Order Items
18
+ orderNumber: { type: String, unique: true, required: true, },
19
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true, },
56
20
  items: [orderItemSchema],
57
-
58
- // Pricing
59
- subtotal: {
60
- type: Number,
61
- required: true,
62
- },
63
- discount: {
64
- type: Number,
65
- default: 0,
66
- },
67
- tax: {
68
- type: Number,
69
- default: 0,
70
- },
71
- shippingCost: {
72
- type: Number,
73
- default: 0,
74
- },
75
- total: {
76
- type: Number,
77
- required: true,
78
- },
79
- currency: {
80
- type: String,
81
- default: "INR",
82
- enum: ["INR", "USD", "EUR", "GBP"],
83
- },
84
-
85
- // Payment Information
86
- paymentMethod: {
87
- type: String,
88
- enum: ["razorpay", "paypal", "card", "upi", "netbanking", "cod"],
89
- required: true,
90
- },
21
+ subtotal: { type: Number, required: true },
22
+ discount: { type: Number, default: 0, },
23
+ tax: { type: Number, default: 0, },
24
+ shippingCost: { type: Number, default: 0, },
25
+ total: { type: Number, required: true, },
26
+ currency: { type: String, default: "INR", enum: ["INR", "USD", "EUR", "GBP"], },
27
+ paymentMethod: { type: String, enum: ["razorpay", "paypal", "card", "upi", "netbanking", "cod"], required: true, },
91
28
  paymentStatus: {
92
29
  type: String,
93
30
  enum: ["pending", "processing", "completed", "failed", "refunded", "partially_refunded"],
@@ -112,8 +49,6 @@ const orderSchema = new mongoose.Schema(
112
49
  },
113
50
  paidAt: Date,
114
51
  },
115
-
116
- // Order Status
117
52
  orderStatus: {
118
53
  type: String,
119
54
  enum: [
@@ -130,62 +65,12 @@ const orderSchema = new mongoose.Schema(
130
65
  default: "pending",
131
66
  index: true,
132
67
  },
133
-
134
- // Shipping Information
135
- shippingAddress: {
136
- name: {
137
- type: String,
138
- required: true,
139
- },
140
- phone: {
141
- type: String,
142
- required: true,
143
- },
144
- email: String,
145
- line1: {
146
- type: String,
147
- required: true,
148
- },
149
- line2: String,
150
- city: {
151
- type: String,
152
- required: true,
153
- },
154
- state: {
155
- type: String,
156
- required: true,
157
- },
158
- postalCode: {
159
- type: String,
160
- required: true,
161
- },
162
- country: {
163
- type: String,
164
- required: true,
165
- default: "IN",
166
- },
167
- landmark: String,
168
- },
169
-
170
- billingAddress: {
171
- name: String,
172
- phone: String,
173
- email: String,
174
- line1: String,
175
- line2: String,
176
- city: String,
177
- state: String,
178
- postalCode: String,
179
- country: String,
180
- },
181
-
182
- // Shipping Details
68
+ shippingAddress: { type: mongoose.Schema.Types.ObjectId, ref: "Address", required: true, },
69
+ billingAddress: { type: mongoose.Schema.Types.ObjectId, ref: "Address", required: true, },
183
70
  shippingProvider: String,
184
71
  trackingNumber: String,
185
72
  estimatedDeliveryDate: Date,
186
73
  actualDeliveryDate: Date,
187
-
188
- // Status History
189
74
  statusHistory: [
190
75
  {
191
76
  status: String,
@@ -200,30 +85,21 @@ const orderSchema = new mongoose.Schema(
200
85
  },
201
86
  },
202
87
  ],
203
-
204
- // Discount/Coupon
205
88
  couponCode: String,
206
89
  couponDiscount: {
207
90
  type: Number,
208
91
  default: 0,
209
92
  },
210
-
211
- // Notes
212
93
  customerNote: String,
213
94
  internalNote: String,
214
-
215
- // Cancellation/Return
216
95
  cancellationReason: String,
217
96
  cancelledAt: Date,
218
97
  cancelledBy: {
219
98
  type: mongoose.Schema.Types.ObjectId,
220
99
  ref: "User",
221
100
  },
222
-
223
101
  returnReason: String,
224
102
  returnedAt: Date,
225
-
226
- // Refund Information
227
103
  refundAmount: Number,
228
104
  refundStatus: {
229
105
  type: String,
@@ -232,8 +108,6 @@ const orderSchema = new mongoose.Schema(
232
108
  },
233
109
  refundedAt: Date,
234
110
  refundTransactionId: String,
235
-
236
- // Metadata
237
111
  metadata: {
238
112
  type: Map,
239
113
  of: mongoose.Schema.Types.Mixed,
@@ -252,6 +126,8 @@ orderSchema.index({ paymentStatus: 1 });
252
126
  orderSchema.index({ "paymentDetails.razorpayOrderId": 1 });
253
127
  orderSchema.index({ "paymentDetails.paypalOrderId": 1 });
254
128
  orderSchema.index({ trackingNumber: 1 });
129
+ orderSchema.index({ shippingAddress: 1 });
130
+ orderSchema.index({ billingAddress: 1 });
255
131
 
256
132
  // Pre-save middleware to generate order number
257
133
  orderSchema.pre("save", async function (next) {
@@ -288,7 +164,9 @@ orderSchema.statics.getUserOrders = async function (userId, options = {}) {
288
164
  .limit(limit)
289
165
  .skip(skip)
290
166
  .populate("items.productId", "name images")
291
- .populate("paymentDetails.paymentMethodId");
167
+ .populate("paymentDetails.paymentMethodId")
168
+ .populate("shippingAddress")
169
+ .populate("billingAddress");
292
170
  };
293
171
 
294
172
  // Static method to get order statistics
package/models/payment.js CHANGED
@@ -3,87 +3,28 @@ const crypto = require("crypto");
3
3
 
4
4
  const paymentMethodSchema = new mongoose.Schema(
5
5
  {
6
- userId: {
7
- type: mongoose.Schema.Types.ObjectId,
8
- ref: "User",
9
- required: true,
10
- index: true,
11
- },
12
- paymentType: {
13
- type: String,
14
- enum: ["razorpay", "paypal", "card", "upi", "netbanking"],
15
- required: true,
16
- },
17
- paymentToken: {
18
- type: String,
19
- required: true,
20
- // This stores the tokenized payment method from the payment gateway
21
- },
22
- isDefault: {
23
- type: Boolean,
24
- default: false,
25
- },
26
- isActive: {
27
- type: Boolean,
28
- default: true,
29
- },
30
- // Card/Payment details (only non-sensitive info for display)
6
+ userId: { type: mongoose.Schema.Types.ObjectId. ref: "User", required: true, index: true, },
7
+ paymentType: { type: String, enum: ["razorpay", "paypal", "card", "upi", "netbanking", "stripe"], required: true, },
8
+ paymentToken: { type: String, required: true, },
9
+ isDefault: { type: Boolean, default: false, },
10
+ isActive: { type: Boolean, default: true, },
31
11
  cardDetails: {
32
- last4: {
33
- type: String,
34
- maxlength: 4,
35
- },
36
- brand: {
37
- type: String, // Visa, Mastercard, Amex, etc.
38
- },
39
- expiryMonth: {
40
- type: String,
41
- maxlength: 2,
42
- },
43
- expiryYear: {
44
- type: String,
45
- maxlength: 4,
46
- },
47
- cardholderName: {
48
- type: String,
49
- },
50
- fingerprint: {
51
- type: String, // Unique identifier for the card
52
- },
12
+ last4: { type: String, maxlength: 4, },
13
+ brand: { type: String, },
14
+ expiryMonth: { type: String, maxlength: 2, },
15
+ expiryYear: { type: String, maxlength: 4, },
16
+ cardholderName: { type: String, },
17
+ fingerprint: { type: String,}
53
18
  },
54
- // PayPal details
55
- paypalDetails: {
56
- email: {
57
- type: String,
58
- },
59
- payerId: {
60
- type: String,
61
- },
19
+ paypalDetails: {
20
+ email: { type: String, },
21
+ payerId: { type: String, },
62
22
  },
63
- // UPI details
64
23
  upiDetails: {
65
- vpa: {
66
- type: String, // Virtual Payment Address (e.g., user@paytm)
67
- },
68
- },
69
- // Billing address
70
- billingAddress: {
71
- name: String,
72
- line1: String,
73
- line2: String,
74
- city: String,
75
- state: String,
76
- postalCode: String,
77
- country: {
78
- type: String,
79
- default: "IN",
80
- },
81
- },
82
- // Metadata for additional information
83
- metadata: {
84
- type: Map,
85
- of: String,
24
+ vpa: { type: String, },
86
25
  },
26
+ billingAddress: { type: mongoose.Schema.Types.ObjectId, ref: "Address", required: true, index: true, },
27
+ metadata: { type: Map, of: String, },
87
28
  },
88
29
  {
89
30
  timestamps: true,
package/models/user.js CHANGED
@@ -1,70 +1,135 @@
1
- const mongoose = require("mongoose");
2
- const bcrypt = require("bcrypt");
3
- const crypto = require("crypto");
1
+ const mongoose = require('mongoose');
2
+ const bcrypt = require('bcrypt');
4
3
 
5
4
  const userSchema = new mongoose.Schema({
6
- name: { type: String, required: true },
7
- firstname: { type: String, required: true },
8
- lastname: { type: String, required: false },
9
- displayname: { type: String, required: false },
10
- email: { type: String, required: true, unique: true },
11
- username: { type: String, required: false, unique: false },
12
- mobile: { type: String, required: false, unique: true },
13
- password: { type: String, required: true },
14
- avatar: { type: Object, required: false, default: {
5
+ email: { type: String, required: true, unique: true, lowercase: true, trim: true,
6
+ match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
7
+ },
8
+ password: { type: String, required: true, minlength: 8, select: false },
9
+ firstName: { type: String, required: true, trim: true },
10
+ lastName: { type: String, required: true, trim: true },
11
+ displayName: { type: String, trim: true },
12
+ avatar: { type: Object, required: false, default: {
15
13
  public_id: '',
16
- secure_url: ''
17
- }},
18
- role: { type: String, default: "customer" },
19
- isBlocked: { type: Boolean, default: false },
20
- address: { type: String },
21
- shipping: { type: String },
22
- wishlist: [{ type: mongoose.Schema.Types.ObjectId, ref: "Product" }],
23
- refreshToken: { type: String },
24
- passwordChangedAt: Date,
25
- passwordResetToken: String,
26
- passwordResetExpires: Date
14
+ secure_url: '',
15
+ }, trim: true
16
+ },
17
+ bio: { type: String, maxlength: 500 },
18
+ dateOfBirth: Date,
19
+ mobile: { type: String, trim: true, unique: false, required: false },
20
+ role: { type: String,
21
+ enum: ['user', 'admin', 'moderator'], default: 'user' },
22
+ status: { type: String,
23
+ enum: ['active', 'inactive', 'suspended', 'deleted'], default: 'active' },
24
+ emailVerified: { type: Boolean, default: false },
25
+ emailVerificationToken: String,
26
+ emailVerificationExpires: Date,
27
+ passwordResetToken: String,
28
+ passwordResetExpires: Date,
29
+ twoFactorSecret: { type: String, select: false},
30
+ twoFactorEnabled: { type: Boolean, default: false },
31
+ preferences: {
32
+ language: { type: String, default: 'en' },
33
+ timezone: { type: String, default: 'UTC' },
34
+ notifications: {
35
+ email: { type: Boolean, default: true },
36
+ push: { type: Boolean, default: true },
37
+ sms: { type: Boolean, default: false }
27
38
  },
28
- { timestamps: true }
29
- );
39
+ theme: { type: String, enum: ['light', 'dark', 'auto'], default: 'auto' }
40
+ },
41
+ socialAccounts: [{
42
+ provider: { type: String,
43
+ enum: ['google', 'facebook', 'github', 'apple']},
44
+ providerId: String,
45
+ email: String,
46
+ connectedAt: {
47
+ type: Date,
48
+ default: Date.now
49
+ }
50
+ }],
51
+ lastLoginAt: Date,
52
+ lastLoginIP: String,
53
+ loginAttempts: { type: Number, default: 0 },
54
+ lockUntil: Date
55
+ }, {
56
+ timestamps: true
57
+ });
58
+
59
+ // Indexes
60
+ userSchema.index({ email: 1 });
61
+ userSchema.index({ 'profile.firstName': 1, 'profile.lastName': 1 });
62
+ userSchema.index({ createdAt: 1 });
63
+ userSchema.index({ status: 1, role: 1 });
64
+
65
+ // Virtual for full name
66
+ userSchema.virtual('profile.fullName').get(function() {
67
+ return `${this.profile.firstName} ${this.profile.lastName}`;
68
+ });
30
69
 
31
- // Hooks
32
- userSchema.pre("save", async function (next) {
33
- if (!this.isModified("password")) {
70
+ // Hash password before saving
71
+ userSchema.pre('save', async function(next) {
72
+ if (!this.isModified('password')) return next();
73
+
74
+ try {
75
+ const salt = await bcrypt.genSalt(12);
76
+ this.password = await bcrypt.hash(this.password, salt);
34
77
  next();
78
+ } catch (error) {
79
+ next(error);
35
80
  }
36
- const salt = await bcrypt.genSaltSync(10);
37
- this.password = await bcrypt.hash(this.password, salt);
38
- next();
39
81
  });
40
82
 
41
- // Methods
42
- userSchema.methods.compare = async function (entered) {
43
- return await bcrypt.compare(entered, this.password);
83
+ // Method to compare passwords
84
+ userSchema.methods.comparePassword = async function(candidatePassword) {
85
+ return await bcrypt.compare(candidatePassword, this.password);
44
86
  };
45
87
 
46
- userSchema.methods.token = async function () {
47
- const resettoken = crypto.randomBytes(32).toString("hex");
48
-
49
- this.passwordResetToken = crypto.createHash("sha256").update(resettoken).digest("hex");
50
- this.passwordResetExpires = Date.now() + 30 * 60 * 1000;
51
-
52
- return resettoken;
88
+ // Method to check if account is locked
89
+ userSchema.methods.isLocked = function() {
90
+ return !!(this.lockUntil && this.lockUntil > Date.now());
53
91
  };
54
92
 
55
- //Statics
56
- userSchema.statics.countUsers = function () {
57
- return this.countDocuments({});
93
+ // Method to increment login attempts
94
+ userSchema.methods.incLoginAttempts = async function() {
95
+ if (this.lockUntil && this.lockUntil < Date.now()) {
96
+ return await this.updateOne({
97
+ $set: { loginAttempts: 1 },
98
+ $unset: { lockUntil: 1 }
99
+ });
100
+ }
101
+
102
+ const updates = { $inc: { loginAttempts: 1 } };
103
+ const maxAttempts = 5;
104
+ const lockTime = 2 * 60 * 60 * 1000; // 2 hours
105
+
106
+ if (this.loginAttempts + 1 >= maxAttempts && !this.isLocked()) {
107
+ updates.$set = { lockUntil: Date.now() + lockTime };
108
+ }
109
+
110
+ return await this.updateOne(updates);
58
111
  };
59
112
 
60
- userSchema.statics.findByEmail = async function (email) {
61
- return await this.findOne({ email });
113
+ // Method to reset login attempts
114
+ userSchema.methods.resetLoginAttempts = async function() {
115
+ return await this.updateOne({
116
+ $set: { loginAttempts: 0 },
117
+ $unset: { lockUntil: 1 }
118
+ });
62
119
  };
63
120
 
64
- // Query
65
- userSchema.query.paginate = function ({ page, limit }) {
66
- const skip = limit * (page - 1);
67
- return this.skip(skip).limit(limit);
121
+ // Remove sensitive data from JSON output
122
+ userSchema.methods.toJSON = function() {
123
+ const obj = this.toObject();
124
+ delete obj.password;
125
+ delete obj.twoFactorSecret;
126
+ delete obj.emailVerificationToken;
127
+ delete obj.emailVerificationExpires;
128
+ delete obj.passwordResetToken;
129
+ delete obj.passwordResetExpires;
130
+ delete obj.loginAttempts;
131
+ delete obj.lockUntil;
132
+ return obj;
68
133
  };
69
134
 
70
- module.exports = mongoose.model("User", userSchema);
135
+ module.exports = mongoose.model('User', userSchema);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feardread/fear",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,15 @@
1
+ const Address = require('../controllers/address');
2
+
3
+ module.exports = ( fear ) => {
4
+ const router = fear.createRouter();
5
+ const handler = fear.getHandler();
6
+
7
+ router.get("/all", handler.async(Address.all));
8
+ router.post("/new", handler.async(Address.create));
9
+ router.route("/:id")
10
+ .get(handler.async(Address.read))
11
+ .put(handler.async(Address.update))
12
+ .delete(handler.async(Address.delete));
13
+
14
+ return router;
15
+ }
package/routes/order.js CHANGED
@@ -5,8 +5,10 @@ module.exports = ( fear ) => {
5
5
  const handler = fear.getHandler();
6
6
  const validator = fear.getValidator();
7
7
 
8
- router.get("/all", Order.list);
9
- router.post("/new", Order.create);
8
+ router.get("/all", handler.async(Order.list));
9
+ router.post("/new", handler.async(Order.create));
10
+ router.post("/create", handler.async(Order.create));
11
+
10
12
  router.route('/:id')
11
13
  .get(Order.read)
12
14
  .put(Order.update);
package/routes/payment.js CHANGED
@@ -5,9 +5,9 @@ module.exports = (fear) => {
5
5
  const handler = fear.getHandler();
6
6
  const validator = fear.getValidator();
7
7
 
8
- router.get("/all", Payment.list);
9
- router.post("/new", Payment.create);
10
- router.post("/:params", Payment.create);
8
+ router.get("/all", handler.async(Payment.list));
9
+ router.post("/new", handler.async(Payment.create));
10
+ router.post("/create", handler.async(Payment.create));
11
11
 
12
12
  router.route('/:id')
13
13
  .get(Payment.read)