@feardread/fear 1.2.1 → 2.0.0
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/FEAR.js +76 -63
- package/FEARServer.js +290 -20
- package/controllers/address.js +9 -0
- package/controllers/auth/index.js +498 -92
- package/controllers/auth/password.js +237 -0
- package/controllers/order.js +0 -1
- package/controllers/payment.js +5 -142
- package/libs/db/index.js +5 -0
- package/libs/emailer/info.js +22 -34
- package/libs/emailer/smtp.js +511 -65
- package/libs/passport/index.js +137 -0
- package/libs/passport.js +22 -0
- package/libs/paypal/index.js +82 -0
- package/libs/stripe/index.js +306 -0
- package/libs/validator/index.js +2 -2
- package/models/address.js +37 -0
- package/models/blog.js +947 -17
- package/models/brand.js +205 -8
- package/models/category.js +498 -7
- package/models/events.js +1 -0
- package/models/order.js +29 -154
- package/models/payment.js +18 -79
- package/models/user.js +116 -49
- package/package.json +1 -1
- package/routes/address.js +16 -0
- package/routes/auth.js +9 -3
- package/routes/mail.js +10 -165
- package/routes/order.js +7 -4
- package/routes/password.js +17 -0
- package/routes/payment.js +4 -8
- package/routes/paypal.js +12 -0
- package/routes/stripe.js +27 -0
- package/libs/passport/passport.js +0 -109
- /package/routes/{events.js → event.js} +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const passport = require("passport");
|
|
2
|
+
const LocalStrategy = require("passport-local").Strategy;
|
|
3
|
+
const FacebookStrategy = require("passport-facebook").Strategy;
|
|
4
|
+
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
|
5
|
+
|
|
6
|
+
const secret = require("../config/secret");
|
|
7
|
+
const User = require("../models/user");
|
|
8
|
+
|
|
9
|
+
passport.serializeUser((user, done) => {
|
|
10
|
+
done(null, user._id); // Only serialize user ID, not entire user object
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
passport.deserializeUser((id, done) => {
|
|
14
|
+
User.findById(id)
|
|
15
|
+
.lean()
|
|
16
|
+
.exec()
|
|
17
|
+
.then((user) => done(null, user))
|
|
18
|
+
.catch((err) => done(err, null));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Finds or creates a user from OAuth profile
|
|
23
|
+
*/
|
|
24
|
+
function findOrCreateOAuthUser(profile, provider, accessToken) {
|
|
25
|
+
const query = { [`${provider}Id`]: profile.id };
|
|
26
|
+
|
|
27
|
+
return User.findOne(query)
|
|
28
|
+
.then((user) => {
|
|
29
|
+
if (user) {
|
|
30
|
+
return user;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create new user
|
|
34
|
+
const userData = {
|
|
35
|
+
[`${provider}Id`]: profile.id,
|
|
36
|
+
[`${provider}AccessToken`]: accessToken,
|
|
37
|
+
username: profile.displayName,
|
|
38
|
+
email: profile._json?.email || profile.emails?.[0]?.value,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (provider === "facebook") {
|
|
42
|
+
userData.tokens = [{ kind: "facebook", token: accessToken }];
|
|
43
|
+
userData.profile = {
|
|
44
|
+
name: profile.displayName,
|
|
45
|
+
picture: `https://graph.facebook.com/${profile.id}/picture?type=large`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return User.create(userData);
|
|
50
|
+
})
|
|
51
|
+
.then((user) => {
|
|
52
|
+
// If user already existed, return immediately
|
|
53
|
+
if (user.createdAt && Date.now() - user.createdAt > 1000) {
|
|
54
|
+
return user;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create cart for new user
|
|
58
|
+
return createUserCart(user._id).then(() => user);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// Strategy Configurations
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Local Strategy (Email/Password Login)
|
|
68
|
+
*/
|
|
69
|
+
passport.use(
|
|
70
|
+
"login",
|
|
71
|
+
new LocalStrategy(
|
|
72
|
+
{
|
|
73
|
+
usernameField: "email",
|
|
74
|
+
passwordField: "password",
|
|
75
|
+
passReqToCallback: true,
|
|
76
|
+
},
|
|
77
|
+
(req, email, password, done) => {
|
|
78
|
+
User.findOne({ email: email.toLowerCase() })
|
|
79
|
+
.then((user) => {
|
|
80
|
+
if (!user) {
|
|
81
|
+
return done(null, false, req.flash("loginMessage", "No user found with this email"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isValidPassword = user.comparePassword(password);
|
|
85
|
+
|
|
86
|
+
if (!isValidPassword) {
|
|
87
|
+
return done(null, false, req.flash("loginMessage", "Incorrect password"));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return done(null, user);
|
|
91
|
+
})
|
|
92
|
+
.catch((err) => done(err));
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Google OAuth Strategy
|
|
99
|
+
*/
|
|
100
|
+
passport.use(
|
|
101
|
+
"google",
|
|
102
|
+
new GoogleStrategy(
|
|
103
|
+
{
|
|
104
|
+
clientID: process.env.GOOGLE_CLIENT_ID,
|
|
105
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
106
|
+
callbackURL: process.env.GOOGLE_CALLBACK_URL || "http://localhost:4000/fear/api/auth/google",
|
|
107
|
+
scope: ["profile", "email"],
|
|
108
|
+
},
|
|
109
|
+
(accessToken, refreshToken, profile, done) => {
|
|
110
|
+
findOrCreateOAuthUser(profile, "google", accessToken)
|
|
111
|
+
.then((user) => done(null, user))
|
|
112
|
+
.catch((err) => done(err, false));
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Facebook OAuth Strategy
|
|
119
|
+
*/
|
|
120
|
+
passport.use(
|
|
121
|
+
"facebook",
|
|
122
|
+
new FacebookStrategy(
|
|
123
|
+
{
|
|
124
|
+
clientID: secret.facebook.clientID,
|
|
125
|
+
clientSecret: secret.facebook.clientSecret,
|
|
126
|
+
callbackURL: secret.facebook.callbackURL,
|
|
127
|
+
profileFields: ["id", "displayName", "email", "picture.type(large)"],
|
|
128
|
+
},
|
|
129
|
+
(accessToken, refreshToken, profile, done) => {
|
|
130
|
+
findOrCreateOAuthUser(profile, "facebook", accessToken)
|
|
131
|
+
.then((user) => done(null, user))
|
|
132
|
+
.catch((err) => done(err, false));
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
module.exports = passport;
|
package/libs/passport.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const passport = require("passport")
|
|
2
|
+
const User = require("../models/user")
|
|
3
|
+
require("dotenv").config();
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// serializing
|
|
7
|
+
passport.serializeUser(function (user, done) {
|
|
8
|
+
done(null, user.id);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// deserializing
|
|
12
|
+
passport.deserializeUser(async function (id, done) {
|
|
13
|
+
try {
|
|
14
|
+
let user = await User.findById(id)
|
|
15
|
+
done(null, user)
|
|
16
|
+
} catch (err) {
|
|
17
|
+
done(err, null);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
module.exports = passport
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const paypal = require('paypal-rest-sdk');
|
|
2
|
+
|
|
3
|
+
module.exports = function ( fear ) {
|
|
4
|
+
const _this = {};
|
|
5
|
+
_this.logger = fear.getLogger();
|
|
6
|
+
_this.validator = fear.getValidator();
|
|
7
|
+
_this.env = fear.getEnvironment();
|
|
8
|
+
|
|
9
|
+
_this.clientId = _this.env.PAYPAL_CLIENT_ID;
|
|
10
|
+
_this.clientSecret = _this.env.PAYPAL_CLIENT_SECRET;
|
|
11
|
+
|
|
12
|
+
if (!_this.clientId || !_this.clientSecret) {
|
|
13
|
+
throw new Error('Missing PayPal client ID or client secret. Please update .env');
|
|
14
|
+
}
|
|
15
|
+
_this.logger.warn('PayPal Payment Handler initialized')
|
|
16
|
+
|
|
17
|
+
_this.initEnvironment = () => {
|
|
18
|
+
return new paypal.core.SandboxEnvironment(_this.clientId, _this.clientSecret);
|
|
19
|
+
};
|
|
20
|
+
_this.initClient = () => {
|
|
21
|
+
return new paypal.core.PayPalHttpClient(_this.initEnvironment());
|
|
22
|
+
};
|
|
23
|
+
_this.handleError = (res, statusCode, message, error) => {
|
|
24
|
+
_this.logger.error(`PayPal Error :: `, error);
|
|
25
|
+
return res.status(statusCode).json({ success: false, message: message, error: error.message || error });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
createPayPalOrder(req, res) {
|
|
30
|
+
const { amount, currency = "USD" } = req.body;
|
|
31
|
+
|
|
32
|
+
// Validate required fields
|
|
33
|
+
if (!amount || isNaN(amount) || amount <= 0) {
|
|
34
|
+
return res.status(400).json({ success: false, message: "Invalid amount. Must be a positive number." });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const request = new paypal.orders.OrdersCreateRequest();
|
|
38
|
+
request.prefer("return=representation");
|
|
39
|
+
request.requestBody({
|
|
40
|
+
intent: "CAPTURE",
|
|
41
|
+
purchase_units: [{
|
|
42
|
+
amount: {
|
|
43
|
+
currency_code: currency,
|
|
44
|
+
value: amount.toString(),
|
|
45
|
+
},
|
|
46
|
+
}],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return _this.initClient()
|
|
50
|
+
.exeute(request)
|
|
51
|
+
.then((result) => {
|
|
52
|
+
_this.logger.info(`PayPal order created successfully: ${result.id}`);
|
|
53
|
+
return res.status(200).json({success: true, orderId: result.id, order: result });
|
|
54
|
+
})
|
|
55
|
+
.catch(error => _this.handleError( res, 500, "Error creating PayPal order", error ));
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
capturePayPalOrder(req, res) {
|
|
59
|
+
const { orderId } = req.body;
|
|
60
|
+
|
|
61
|
+
if (!orderId || typeof orderId !== 'string') {
|
|
62
|
+
return res.status(400).json({ success: false, message: "Invalid or missing orderId" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const request = new paypal.orders.OrdersCaptureRequest(orderId);
|
|
66
|
+
request.requestBody({});
|
|
67
|
+
|
|
68
|
+
return _this.initClient()
|
|
69
|
+
.execute(request)
|
|
70
|
+
.then((result) => {
|
|
71
|
+
_this.logger.info(`PayPal order captured successfully: ${result.id}`);
|
|
72
|
+
return res.status(200).json({
|
|
73
|
+
success: true,
|
|
74
|
+
captureId: result.id,
|
|
75
|
+
status: result.status,
|
|
76
|
+
capture: result
|
|
77
|
+
});
|
|
78
|
+
})
|
|
79
|
+
.catch(error => _this.handleError( res, 500, "Error capturing PayPal order", error ));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
const stripe = require('stripe');
|
|
2
|
+
|
|
3
|
+
module.exports = function ( fear ) {
|
|
4
|
+
const _this = {};
|
|
5
|
+
_this.env = fear.getEnvironment();
|
|
6
|
+
_this.logger = fear.getLogger();
|
|
7
|
+
|
|
8
|
+
if (!_this.env.STRIPE_SECRET_KEY) {
|
|
9
|
+
throw new Error('Missing STRIPE_SECRET_KEY. Please update .env');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_this.stripe = stripe(_this.env.STRIPE_SECRET_KEY);
|
|
13
|
+
_this.webhookSecret = _this.env.STRIPE_WEBHOOK_SECRET;
|
|
14
|
+
_this.logger.warn('Stripe Payment Handler initialized');
|
|
15
|
+
|
|
16
|
+
_this.handleError = (res, statusCode, error) => {
|
|
17
|
+
_this.logger.error(`Stripe Error :: `, error);
|
|
18
|
+
return res.status(statusCode).json({ success: false, message: error.message, error });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
_this.handleSuccess = (res, statusCode, data) => {
|
|
22
|
+
_this.logger.info(`Stripe Success ::`, data);
|
|
23
|
+
return res.status(statusCode).json({ success: true, message: "Stripe Success", result: data });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
|
|
28
|
+
createPaymentIntent: (req, res) => {
|
|
29
|
+
const { currency, amount, metadata } = req.body;
|
|
30
|
+
|
|
31
|
+
if (!amount || amount <= 0) {
|
|
32
|
+
return _this.handleError(res, 400, {message: 'Amount is required and must be greater than 0'})
|
|
33
|
+
}
|
|
34
|
+
const paymentIntentParams = {
|
|
35
|
+
amount,
|
|
36
|
+
metadata,
|
|
37
|
+
currency: (currency) ? currency.toLowerCase() : 'usd',
|
|
38
|
+
automatic_payment_methods: { enabled: true }
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return _this.stripe.paymentIntents
|
|
42
|
+
.create(paymentIntentParams)
|
|
43
|
+
.then(result => _this.handleSuccess(res, 200, result))
|
|
44
|
+
.catch(error => _this.handleError(res, 500, error));
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
retrievePaymentIntent: (req, res) => {
|
|
48
|
+
const { id } = req.params;
|
|
49
|
+
|
|
50
|
+
if (!id) {
|
|
51
|
+
return res.status(400).json({ success: false, message: 'Payment intent ID is required'});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return _this.stripe.paymentIntents.retrieve(id)
|
|
55
|
+
.then(result => _this.handleSuccess(res, 200, result))
|
|
56
|
+
.catch(error => _this.handleError(res, 500, error));
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
cancelPaymentIntent: (req, res) => {
|
|
60
|
+
const { id } = req.params;
|
|
61
|
+
|
|
62
|
+
if (!id) {
|
|
63
|
+
return res.status(400).json({
|
|
64
|
+
success: false,
|
|
65
|
+
message: 'Payment intent ID is required'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return _this.stripe.paymentIntents.cancel(id)
|
|
70
|
+
.then(result => {
|
|
71
|
+
return _this.handleSuccess(res, 200, 'Payment intent cancelled', result);
|
|
72
|
+
})
|
|
73
|
+
.catch(error => {
|
|
74
|
+
return _this.handleError(res, error.statusCode || 500, 'Failed to cancel payment intent', error, 'Error cancelling payment intent');
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
createCustomer: (req, res) => {
|
|
79
|
+
const { email, name, metadata } = req.body;
|
|
80
|
+
|
|
81
|
+
if (!email) {
|
|
82
|
+
return res.status(400).json({
|
|
83
|
+
success: false,
|
|
84
|
+
message: 'Email is required'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return _this.stripe.customers.create({
|
|
89
|
+
email,
|
|
90
|
+
name,
|
|
91
|
+
metadata
|
|
92
|
+
})
|
|
93
|
+
.then(result => {
|
|
94
|
+
return _this.handleSuccess(res, 200, 'Customer created successfully', result);
|
|
95
|
+
})
|
|
96
|
+
.catch(error => {
|
|
97
|
+
return _this.handleError(res, 500, 'Failed to create customer', error, 'Error creating customer');
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
retrieveCustomer: (req, res) => {
|
|
102
|
+
const { id } = req.params;
|
|
103
|
+
|
|
104
|
+
if (!id) {
|
|
105
|
+
return res.status(400).json({
|
|
106
|
+
success: false,
|
|
107
|
+
message: 'Customer ID is required'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return _this.stripe.customers.retrieve(id)
|
|
112
|
+
.then(result => {
|
|
113
|
+
return _this.handleSuccess(res, 200, 'Customer retrieved', result);
|
|
114
|
+
})
|
|
115
|
+
.catch(error => {
|
|
116
|
+
return _this.handleError(res, error.statusCode || 500, 'Failed to retrieve customer', error, 'Error retrieving customer');
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
listCustomers: (req, res) => {
|
|
121
|
+
const { limit = 10 } = req.query;
|
|
122
|
+
const parsedLimit = Number(limit);
|
|
123
|
+
|
|
124
|
+
if (isNaN(parsedLimit) || parsedLimit <= 0) {
|
|
125
|
+
return res.status(400).json({
|
|
126
|
+
success: false,
|
|
127
|
+
message: 'Limit must be a positive number'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return _this.stripe.customers.list({ limit: parsedLimit })
|
|
132
|
+
.then(result => {
|
|
133
|
+
_this.logger.info(`Customers retrieved :: count: ${result.data.length}`);
|
|
134
|
+
return res.status(200).json({
|
|
135
|
+
success: true,
|
|
136
|
+
message: 'Customers retrieved',
|
|
137
|
+
result: result.data,
|
|
138
|
+
count: result.data.length
|
|
139
|
+
});
|
|
140
|
+
})
|
|
141
|
+
.catch(error => {
|
|
142
|
+
return _this.handleError(res, 500, 'Failed to list customers', error, 'Error listing customers');
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
createRefund: (req, res) => {
|
|
147
|
+
const { paymentIntentId, amount, reason } = req.body;
|
|
148
|
+
|
|
149
|
+
if (!paymentIntentId) {
|
|
150
|
+
return res.status(400).json({
|
|
151
|
+
success: false,
|
|
152
|
+
message: 'Payment intent ID is required'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const refundParams = {
|
|
157
|
+
payment_intent: paymentIntentId
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (amount) refundParams.amount = amount;
|
|
161
|
+
if (reason) refundParams.reason = reason;
|
|
162
|
+
|
|
163
|
+
return _this.stripe.refunds.create(refundParams)
|
|
164
|
+
.then(result => {
|
|
165
|
+
return _this.handleSuccess(res, 200, 'Refund created successfully', result);
|
|
166
|
+
})
|
|
167
|
+
.catch(error => {
|
|
168
|
+
return _this.handleError(res, error.statusCode || 500, 'Failed to create refund', error, 'Error creating refund');
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
createCheckoutSession: (req, res) => {
|
|
173
|
+
const { lineItems, successUrl, cancelUrl, metadata, customer } = req.body;
|
|
174
|
+
|
|
175
|
+
if (!lineItems || !Array.isArray(lineItems) || lineItems.length === 0) {
|
|
176
|
+
return res.status(400).json({
|
|
177
|
+
success: false,
|
|
178
|
+
message: 'Line items are required and must be a non-empty array'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!successUrl || !cancelUrl) {
|
|
183
|
+
return res.status(400).json({
|
|
184
|
+
success: false,
|
|
185
|
+
message: 'Success and cancel URLs are required'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sessionParams = {
|
|
190
|
+
line_items: lineItems,
|
|
191
|
+
mode: 'payment',
|
|
192
|
+
success_url: successUrl,
|
|
193
|
+
cancel_url: cancelUrl
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (metadata) sessionParams.metadata = metadata;
|
|
197
|
+
if (customer) sessionParams.customer = customer;
|
|
198
|
+
|
|
199
|
+
return _this.stripe.checkout.sessions.create(sessionParams)
|
|
200
|
+
.then(result => {
|
|
201
|
+
return _this.handleSuccess(res, 200, 'Checkout session created', result);
|
|
202
|
+
})
|
|
203
|
+
.catch(error => {
|
|
204
|
+
return _this.handleError(res, error.statusCode || 500, 'Failed to create checkout session', error, 'Error creating checkout session');
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
createSubscription: (req, res) => {
|
|
209
|
+
const { customerId, priceId, metadata } = req.body;
|
|
210
|
+
|
|
211
|
+
if (!customerId || !priceId) {
|
|
212
|
+
return res.status(400).json({
|
|
213
|
+
success: false,
|
|
214
|
+
message: 'Customer ID and Price ID are required'
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const subscriptionParams = {
|
|
219
|
+
customer: customerId,
|
|
220
|
+
items: [{ price: priceId }]
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (metadata) subscriptionParams.metadata = metadata;
|
|
224
|
+
|
|
225
|
+
return _this.stripe.subscriptions.create(subscriptionParams)
|
|
226
|
+
.then(result => {
|
|
227
|
+
return _this.handleSuccess(res, 200, 'Subscription created successfully', result);
|
|
228
|
+
})
|
|
229
|
+
.catch(error => {
|
|
230
|
+
return _this.handleError(res, error.statusCode || 500, 'Failed to create subscription', error, 'Error creating subscription');
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
cancelSubscription: (req, res) => {
|
|
235
|
+
const { id } = req.params;
|
|
236
|
+
|
|
237
|
+
if (!id) {
|
|
238
|
+
return _this.handleError(res, 400, {message: 'Subscription ID is required'})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return _this.stripe.subscriptions.cancel(id)
|
|
242
|
+
.then(result => _this.handleSuccess(res, 200, 'Subscription cancelled', result))
|
|
243
|
+
.catch(error => _this.handleError(res, error.statusCode || 500, 'Failed to cancel subscription', error, 'Error cancelling subscription'));
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
handleWebhook: (req, res) => {
|
|
247
|
+
const sig = req.headers['stripe-signature'];
|
|
248
|
+
|
|
249
|
+
if (!sig) {
|
|
250
|
+
return res.status(400).json({
|
|
251
|
+
success: false,
|
|
252
|
+
message: 'Missing stripe-signature header'
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!_this.webhookSecret) {
|
|
257
|
+
_this.logger.warn('Webhook secret not configured');
|
|
258
|
+
return res.status(400).json({
|
|
259
|
+
success: false,
|
|
260
|
+
message: 'Webhook secret not configured'
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let event;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
event = _this.stripe.webhooks.constructEvent(
|
|
268
|
+
req.body,
|
|
269
|
+
sig,
|
|
270
|
+
_this.webhookSecret
|
|
271
|
+
);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return _this.handleError(res, 400, 'Webhook signature verification failed', error, 'Webhook verification error');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_this.logger.info(`Webhook received: ${event.type}`);
|
|
277
|
+
|
|
278
|
+
return res.status(200).json({
|
|
279
|
+
success: true,
|
|
280
|
+
received: true
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
saveStripePayment: (req, res) => {
|
|
285
|
+
const { paymentMethodId, customerId } = req.body;
|
|
286
|
+
|
|
287
|
+
if (!paymentMethodId || !customerId) {
|
|
288
|
+
return res.status(400).json({success: false, message: 'Payment method ID and customer ID are required' });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return _this.stripe.paymentMethods.attach(paymentMethodId, { customer: customerId })
|
|
292
|
+
.then(() => {
|
|
293
|
+
return _this.stripe.customers.update(customerId, {
|
|
294
|
+
invoice_settings: {
|
|
295
|
+
default_payment_method: paymentMethodId,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
})
|
|
299
|
+
.then((result) => {
|
|
300
|
+
_this.logger.info('Stripe payment method saved :: ', result.id);
|
|
301
|
+
return _this.handleSuccess(res, 200, result)
|
|
302
|
+
})
|
|
303
|
+
.catch(error => _this.handleError(res, error.statusCode || 500, 'Failed to save payment method', error, 'Error saving payment method'));
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
};
|
package/libs/validator/index.js
CHANGED
|
@@ -52,7 +52,7 @@ exports.input = {
|
|
|
52
52
|
* @returns {object} Validation result
|
|
53
53
|
*/
|
|
54
54
|
register: (data) => {
|
|
55
|
-
const { email, password,
|
|
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 =
|
|
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
|
+
zipCode: { 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);
|