@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.
@@ -0,0 +1,139 @@
1
+ const mongoose = require("mongoose");
2
+ const crypto = require("crypto");
3
+
4
+ const paymentMethodSchema = new mongoose.Schema(
5
+ {
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, },
11
+ cardDetails: {
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,}
18
+ },
19
+ paypalDetails: {
20
+ email: { type: String, },
21
+ payerId: { type: String, },
22
+ },
23
+ upiDetails: {
24
+ vpa: { type: String, },
25
+ },
26
+ billingAddress: { type: mongoose.Schema.Types.ObjectId, ref: "Address", required: true, index: true, },
27
+ metadata: { type: Map, of: String, },
28
+ },
29
+ {
30
+ timestamps: true,
31
+ }
32
+ );
33
+
34
+ // Indexes for better query performance
35
+ paymentMethodSchema.index({ userId: 1, isDefault: 1 });
36
+ paymentMethodSchema.index({ userId: 1, isActive: 1 });
37
+ paymentMethodSchema.index({ createdAt: -1 });
38
+
39
+ // Middleware to ensure only one default payment method per user
40
+ paymentMethodSchema.pre("save", async function (next) {
41
+ if (this.isDefault && this.isModified("isDefault")) {
42
+ // Remove default flag from other payment methods for this user
43
+ await mongoose.model("PaymentMethod").updateMany(
44
+ {
45
+ userId: this.userId,
46
+ _id: { $ne: this._id },
47
+ isDefault: true,
48
+ },
49
+ { isDefault: false }
50
+ );
51
+ }
52
+ next();
53
+ });
54
+ paymentMethodSchema.statics.getDefaultPaymentMethod = async function (userId) {
55
+ return await this.findOne({
56
+ userId,
57
+ isDefault: true,
58
+ isActive: true,
59
+ });
60
+ };
61
+ paymentMethodSchema.statics.getUserPaymentMethods = async function (userId) {
62
+ return await this.find({
63
+ userId,
64
+ isActive: true,
65
+ }).sort({ isDefault: -1, createdAt: -1 });
66
+ };
67
+
68
+ paymentMethodSchema.methods.setAsDefault = async function () {
69
+ await mongoose.model("PaymentMethod").updateMany(
70
+ {
71
+ userId: this.userId,
72
+ _id: { $ne: this._id },
73
+ },
74
+ { isDefault: false }
75
+ );
76
+
77
+ this.isDefault = true;
78
+ return await this.save();
79
+ };
80
+ paymentMethodSchema.methods.softDelete = async function () {
81
+ this.isActive = false;
82
+ if (this.isDefault) {
83
+ this.isDefault = false;
84
+ // Optionally set another method as default
85
+ const otherMethod = await mongoose.model("PaymentMethod").findOne({
86
+ userId: this.userId,
87
+ _id: { $ne: this._id },
88
+ isActive: true,
89
+ });
90
+ if (otherMethod) {
91
+ otherMethod.isDefault = true;
92
+ await otherMethod.save();
93
+ }
94
+ }
95
+ return await this.save();
96
+ };
97
+
98
+ // Virtual for masked card number display
99
+ paymentMethodSchema.virtual("displayNumber").get(function () {
100
+ if (this.cardDetails?.last4) {
101
+ return `•••• •••• •••• ${this.cardDetails.last4}`;
102
+ }
103
+ if (this.paypalDetails?.email) {
104
+ return this.paypalDetails.email;
105
+ }
106
+ if (this.upiDetails?.vpa) {
107
+ return this.upiDetails.vpa;
108
+ }
109
+ return "Payment Method";
110
+ });
111
+
112
+ // Virtual for expiry display
113
+ paymentMethodSchema.virtual("expiryDisplay").get(function () {
114
+ if (this.cardDetails?.expiryMonth && this.cardDetails?.expiryYear) {
115
+ return `${this.cardDetails.expiryMonth}/${this.cardDetails.expiryYear}`;
116
+ }
117
+ return null;
118
+ });
119
+
120
+ // Virtual for checking if card is expired
121
+ paymentMethodSchema.virtual("isExpired").get(function () {
122
+ if (this.cardDetails?.expiryMonth && this.cardDetails?.expiryYear) {
123
+ const now = new Date();
124
+ const expiryDate = new Date(
125
+ parseInt(this.cardDetails.expiryYear),
126
+ parseInt(this.cardDetails.expiryMonth) - 1
127
+ );
128
+ return now > expiryDate;
129
+ }
130
+ return false;
131
+ });
132
+
133
+ // Ensure virtuals are included in JSON
134
+ paymentMethodSchema.set("toJSON", { virtuals: true });
135
+ paymentMethodSchema.set("toObject", { virtuals: true });
136
+
137
+ const PaymentMethod = mongoose.model("PaymentMethod", paymentMethodSchema);
138
+
139
+ module.exports = PaymentMethod;
package/models/user.js CHANGED
@@ -1,68 +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
- lastname: { type: String, required: false },
8
- email: { type: String, required: true, unique: true },
9
- username: { type: String, required: false, unique: false },
10
- mobile: { type: String, required: false, unique: true },
11
- password: { type: String, required: true },
12
- 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: {
13
13
  public_id: '',
14
- secure_url: ''
15
- }},
16
- role: { type: String, default: "customer" },
17
- isBlocked: { type: Boolean, default: false },
18
- address: { type: String },
19
- shipping: { type: String },
20
- wishlist: [{ type: mongoose.Schema.Types.ObjectId, ref: "Product" }],
21
- refreshToken: { type: String },
22
- passwordChangedAt: Date,
23
- passwordResetToken: String,
24
- 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 }
25
38
  },
26
- { timestamps: true }
27
- );
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
+ });
28
69
 
29
- // Hooks
30
- userSchema.pre("save", async function (next) {
31
- 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);
32
77
  next();
78
+ } catch (error) {
79
+ next(error);
33
80
  }
34
- const salt = await bcrypt.genSaltSync(10);
35
- this.password = await bcrypt.hash(this.password, salt);
36
- next();
37
81
  });
38
82
 
39
- // Methods
40
- userSchema.methods.compare = async function (entered) {
41
- 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);
42
86
  };
43
87
 
44
- userSchema.methods.token = async function () {
45
- const resettoken = crypto.randomBytes(32).toString("hex");
46
-
47
- this.passwordResetToken = crypto.createHash("sha256").update(resettoken).digest("hex");
48
- this.passwordResetExpires = Date.now() + 30 * 60 * 1000;
49
-
50
- return resettoken;
88
+ // Method to check if account is locked
89
+ userSchema.methods.isLocked = function() {
90
+ return !!(this.lockUntil && this.lockUntil > Date.now());
51
91
  };
52
92
 
53
- //Statics
54
- userSchema.statics.countUsers = function () {
55
- 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);
56
111
  };
57
112
 
58
- userSchema.statics.findByEmail = async function (email) {
59
- 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
+ });
60
119
  };
61
120
 
62
- // Query
63
- userSchema.query.paginate = function ({ page, limit }) {
64
- const skip = limit * (page - 1);
65
- 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;
66
133
  };
67
134
 
68
- 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.4",
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);
@@ -0,0 +1,18 @@
1
+ const Payment = require('../controllers/payment');
2
+
3
+ module.exports = (fear) => {
4
+ const router = fear.createRouter();
5
+ const handler = fear.getHandler();
6
+ const validator = fear.getValidator();
7
+
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
+
12
+ router.route('/:id')
13
+ .get(Payment.read)
14
+ .put(Payment.update);
15
+
16
+
17
+ return router;
18
+ }