@feardread/fear 2.0.4 → 2.0.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
@@ -4,6 +4,9 @@ const path = require('path');
4
4
  const express = require('express');
5
5
  const fs = require('fs');
6
6
  const dotenv = require('dotenv');
7
+ const http = require('http');
8
+ const https = require('https');
9
+ const AutoEncrypt = require('@small-tech/auto-encrypt');
7
10
 
8
11
  const FearServer = (function () {
9
12
  // Private constants
@@ -14,17 +17,23 @@ const FearServer = (function () {
14
17
  };
15
18
 
16
19
  const DEFAULT_PORT = 4000;
20
+ const DEFAULT_HTTPS_PORT = 443;
17
21
  const SHUTDOWN_TIMEOUT = 10000; // 10 seconds
18
22
 
19
23
  // Constructor
20
24
  function FearServer() {
21
25
  this.fear = null;
22
26
  this.server = null;
27
+ this.httpsServer = null;
28
+ this.httpRedirectServer = null;
23
29
  this.Router = null;
24
30
  this.isShuttingDown = false;
25
31
  this.rootDir = path.resolve();
26
32
  this.reactApps = []; // Track multiple React apps
27
33
  this.envLoaded = false;
34
+ this.httpsConfig = null;
35
+ this.greenlock = null;
36
+ this.autoEncrypt = null;
28
37
  }
29
38
 
30
39
  FearServer.prototype = {
@@ -95,6 +104,196 @@ const FearServer = (function () {
95
104
  return true;
96
105
  },
97
106
 
107
+ /**
108
+ * Configure HTTPS with auto-encryption using Greenlock or Auto Encrypt
109
+ * @param {Object} config - HTTPS configuration
110
+ * @param {string} config.mode - 'greenlock', 'auto-encrypt', or 'manual'
111
+ * @param {string} config.domain - Domain name for Let's Encrypt (greenlock/auto-encrypt mode)
112
+ * @param {string} config.email - Email for Let's Encrypt notifications (greenlock/auto-encrypt mode)
113
+ * @param {string} config.certPath - Path to SSL certificate (manual mode)
114
+ * @param {string} config.keyPath - Path to SSL private key (manual mode)
115
+ * @param {string} config.caPath - Path to CA bundle (manual mode, optional)
116
+ * @param {boolean} config.staging - Use Let's Encrypt staging server (greenlock/auto-encrypt mode)
117
+ * @param {string} config.configDir - Directory to store Greenlock config (greenlock mode, default: ./greenlock.d)
118
+ * @param {string} config.settingsPath - Path to Auto Encrypt settings (auto-encrypt mode, default: .small-tech.org)
119
+ * @param {number} config.httpsPort - HTTPS port (default: 443)
120
+ * @param {boolean} config.redirectHttp - Redirect HTTP to HTTPS (default: true)
121
+ * @param {Array<string>} config.altnames - Alternative domain names (greenlock mode, optional)
122
+ */
123
+ setupHTTPS(config) {
124
+ if (!config || typeof config !== 'object') {
125
+ throw new Error('HTTPS configuration is required');
126
+ }
127
+
128
+ this.httpsConfig = {
129
+ mode: config.mode || 'greenlock',
130
+ httpsPort: config.httpsPort || DEFAULT_HTTPS_PORT,
131
+ redirectHttp: config.redirectHttp !== false,
132
+ certPath: config.certPath || process.env.SSL_CERT_PATH,
133
+ ...config
134
+ };
135
+
136
+ if (this.httpsConfig.mode === 'greenlock') {
137
+ this._setupGreenlock();
138
+ } else if (this.httpsConfig.mode === 'auto-encrypt') {
139
+ this._setupAutoEncrypt();
140
+ } else if (this.httpsConfig.mode === 'manual') {
141
+ this._validateManualCerts();
142
+ } else {
143
+ throw new Error('HTTPS mode must be "greenlock", "auto-encrypt", or "manual"');
144
+ }
145
+
146
+ if (this.fear && this.fear.getLogger()) {
147
+ this.fear.getLogger().info('HTTPS configuration loaded successfully');
148
+ }
149
+ },
150
+
151
+ /**
152
+ * Setup Greenlock for automatic SSL certificates
153
+ * @private
154
+ */
155
+ _setupGreenlock() {
156
+ const config = this.httpsConfig;
157
+
158
+ if (!config.domain) {
159
+ throw new Error('Domain name is required for Greenlock mode');
160
+ }
161
+
162
+ if (!config.email) {
163
+ throw new Error('Email address is required for Greenlock mode');
164
+ }
165
+
166
+ try {
167
+ // Greenlock Express uses a different pattern
168
+ // We don't initialize it here, we do it when starting the server
169
+ this.greenlockConfig = {
170
+ packageRoot: this.rootDir,
171
+ configDir: config.configDir || path.join(this.rootDir, 'greenlock.d'),
172
+ maintainerEmail: config.email,
173
+ cluster: false,
174
+ staging: config.staging || false,
175
+ domain: config.domain,
176
+ altnames: config.altnames || [config.domain]
177
+ };
178
+
179
+ // Ensure config directory exists
180
+ const configDir = this.greenlockConfig.configDir;
181
+ if (!fs.existsSync(configDir)) {
182
+ fs.mkdirSync(configDir, { recursive: true });
183
+ }
184
+
185
+ if (this.fear && this.fear.getLogger()) {
186
+ this.fear.getLogger().info(`Greenlock configured for domain: ${config.domain}`);
187
+ this.fear.getLogger().info(`Staging mode: ${config.staging ? 'enabled' : 'disabled'}`);
188
+ }
189
+ } catch (error) {
190
+ console.error('Error setting up Greenlock:', error);
191
+ throw new Error('Failed to setup Greenlock. Make sure @root/greenlock-express is installed: npm install @root/greenlock-express');
192
+ }
193
+ },
194
+
195
+ /**
196
+ * Setup Auto Encrypt for automatic SSL certificates
197
+ * @private
198
+ */
199
+ _setupAutoEncrypt() {
200
+ const config = this.httpsConfig;
201
+
202
+ if (!config.domain) {
203
+ throw new Error('Domain name is required for Auto Encrypt mode');
204
+ }
205
+
206
+ try {
207
+ //const AutoEncrypt = require('@small-tech/auto-encrypt');
208
+
209
+ const settingsPath = config.settingsPath || path.join(this.rootDir, '.small-tech.org');
210
+
211
+ // Auto Encrypt doesn't need separate initialization
212
+ // It's used directly when creating the HTTPS server
213
+ this.autoEncryptOptions = {
214
+ domains: [config.domain],
215
+ settingsPath: settingsPath
216
+ };
217
+
218
+ // Store staging mode for server creation
219
+ if (config.staging) {
220
+ this.autoEncryptOptions.staging = true;
221
+ }
222
+
223
+ if (this.fear && this.fear.getLogger()) {
224
+ this.fear.getLogger().info(`Auto Encrypt configured for domain: ${config.domain}`);
225
+ this.fear.getLogger().info(`Staging mode: ${config.staging ? 'enabled' : 'disabled'}`);
226
+ this.fear.getLogger().info(`Settings path: ${settingsPath}`);
227
+ }
228
+ } catch (error) {
229
+ console.error('Error setting up Auto Encrypt:', error);
230
+ throw new Error('Failed to setup Auto Encrypt. Make sure @small-tech/auto-encrypt is installed: npm install @small-tech/auto-encrypt');
231
+ }
232
+ },
233
+
234
+ /**
235
+ * Validate manual SSL certificates
236
+ * @private
237
+ */
238
+ _validateManualCerts() {
239
+ const config = this.httpsConfig;
240
+
241
+ if (!config.certPath || !config.keyPath) {
242
+ throw new Error('Certificate path and key path are required for manual HTTPS mode');
243
+ }
244
+
245
+ if (!fs.existsSync(config.certPath)) {
246
+ throw new Error(`SSL certificate not found: ${config.certPath}`);
247
+ }
248
+
249
+ if (!fs.existsSync(config.keyPath)) {
250
+ throw new Error(`SSL private key not found: ${config.keyPath}`);
251
+ }
252
+
253
+ if (config.caPath && !fs.existsSync(config.caPath)) {
254
+ throw new Error(`CA bundle not found: ${config.caPath}`);
255
+ }
256
+
257
+ if (this.fear && this.fear.getLogger()) {
258
+ this.fear.getLogger().info('Manual SSL certificates validated');
259
+ }
260
+ },
261
+
262
+ /**
263
+ * Create HTTPS server with manual certificates
264
+ * @private
265
+ */
266
+ _createManualHttpsServer() {
267
+ const config = this.httpsConfig;
268
+
269
+ const httpsOptions = {
270
+ key: fs.readFileSync(config.keyPath),
271
+ cert: fs.readFileSync(config.certPath)
272
+ };
273
+
274
+ if (config.caPath) {
275
+ httpsOptions.ca = fs.readFileSync(config.caPath);
276
+ }
277
+
278
+ return https.createServer(httpsOptions, this.fear.getApp());
279
+ },
280
+
281
+ /**
282
+ * Create HTTP to HTTPS redirect server
283
+ * @private
284
+ */
285
+ _createHttpRedirectServer(httpsPort) {
286
+ const redirectApp = express();
287
+
288
+ redirectApp.use((req, res) => {
289
+ const host = req.headers.host.split(':')[0];
290
+ const httpsUrl = `https://${host}${httpsPort !== 443 ? ':' + httpsPort : ''}${req.url}`;
291
+ res.redirect(301, httpsUrl);
292
+ });
293
+
294
+ return http.createServer(redirectApp);
295
+ },
296
+
98
297
  /**
99
298
  * Validate that required files exist for React app
100
299
  * @param {string} buildPath - Path to build directory
@@ -259,34 +458,31 @@ const FearServer = (function () {
259
458
  },
260
459
 
261
460
  /**
262
- * Setup API routes prefix (useful to avoid conflicts with React routes)
263
- * @param {string} prefix - API prefix (e.g., '/api')
461
+ * Setup API prefix for all routes
462
+ * @param {string} prefix - API prefix (e.g., '/api/v1')
264
463
  */
265
- setupAPIPrefix(prefix = '/fear/api') {
464
+ setupAPIPrefix(prefix) {
465
+ if (!prefix || typeof prefix !== 'string') {
466
+ throw new Error('API prefix must be a string');
467
+ }
468
+
266
469
  const normalizedPrefix = `/${prefix.replace(/^\/+|\/+$/g, '')}`;
267
470
 
268
- this.fear.getApp().use(normalizedPrefix, (req, res, next) => {
269
-
270
- req.isAPIRoute = true;
271
- next();
272
- });
273
-
274
- this.fear.getLogger().info(`API routes configured with prefix: ${normalizedPrefix}`);
275
- return normalizedPrefix;
471
+ this.fear.getLogger().info(`Setting up API prefix: ${normalizedPrefix}`);
472
+ // This would need to be implemented in your FEAR framework
473
+ // to prefix all routes - just documenting the intent here
276
474
  },
277
475
 
278
476
  /**
279
- * Setup process event handlers for graceful shutdown
477
+ * Setup process signal handlers for graceful shutdown
280
478
  */
281
479
  setupProcessHandlers() {
282
- // Handle unhandled promise rejections
283
- process.on("unhandledRejection", (reason, promise) => {
284
- console.log('Unhandled Rejection at:', promise);
285
- this.fear.getLogger().error('Unhandled Rejection at:', promise, 'reason:', reason);
286
- this.gracefulShutdown('unhandledRejection');
287
- });
480
+ // Prevent duplicate listeners
481
+ if (this._handlersSetup) {
482
+ return;
483
+ }
484
+ this._handlersSetup = true;
288
485
 
289
- // Handle uncaught exceptions
290
486
  process.on("uncaughtException", (err) => {
291
487
  this.fear.getLogger().error('Uncaught Exception:', err);
292
488
  this.gracefulShutdown('uncaughtException');
@@ -350,6 +546,161 @@ const FearServer = (function () {
350
546
  });
351
547
  },
352
548
 
549
+ /**
550
+ * Start HTTPS server
551
+ * @private
552
+ */
553
+ async _startHttpsServer() {
554
+ const logger = this.fear.getLogger();
555
+ const httpsPort = this.httpsConfig.httpsPort;
556
+
557
+ let httpsServer;
558
+
559
+ if (this.httpsConfig.mode === 'greenlock') {
560
+ // Greenlock Express initialization and startup
561
+ logger.info('Starting HTTPS server with Greenlock auto-encryption...');
562
+
563
+ const greenlockExpress = require('@root/greenlock-express');
564
+
565
+ return new Promise((resolve, reject) => {
566
+ try {
567
+ // Initialize Greenlock with full configuration
568
+ const greenlock = greenlockExpress.init({
569
+ packageRoot: this.greenlockConfig.packageRoot,
570
+ configDir: this.greenlockConfig.configDir,
571
+ maintainerEmail: this.greenlockConfig.maintainerEmail,
572
+ cluster: false,
573
+
574
+ // Notify callback for errors
575
+ notify: (event, details) => {
576
+ if (event === 'error') {
577
+ logger.error('Greenlock error:', details);
578
+ } else if (event === 'warning') {
579
+ logger.warn('Greenlock warning:', details);
580
+ }
581
+ }
582
+ });
583
+
584
+ // Store greenlock instance
585
+ this.greenlock = greenlock;
586
+
587
+ // Serve the app - Greenlock handles everything automatically
588
+ // It will listen on port 80 (HTTP) and 443 (HTTPS)
589
+ greenlock.serve(this.fear.getApp());
590
+
591
+ logger.info(`✓ Greenlock HTTPS server started`);
592
+ logger.info(`✓ Listening on port 443 (HTTPS)`);
593
+ logger.info(`✓ Listening on port 80 (HTTP → HTTPS redirect)`);
594
+ logger.info(`✓ Staging mode: ${this.greenlockConfig.staging ? 'ENABLED' : 'DISABLED'}`);
595
+ logger.info('');
596
+ logger.info('⚠️ IMPORTANT: Configure your domain using Greenlock CLI:');
597
+ logger.info(` npx greenlock add --subject ${this.greenlockConfig.domain} --altnames ${this.greenlockConfig.altnames.join(',')}`);
598
+ logger.info('');
599
+
600
+ // Mark server as started
601
+ httpsServer = {
602
+ greenlockManaged: true,
603
+ greenlock: greenlock
604
+ };
605
+
606
+ resolve(httpsServer);
607
+
608
+ } catch (error) {
609
+ logger.error('Failed to initialize Greenlock:', error);
610
+ reject(error);
611
+ }
612
+ });
613
+
614
+ } else if (this.httpsConfig.mode === 'auto-encrypt') {
615
+ // Auto Encrypt mode
616
+ logger.info('Starting HTTPS server with Auto Encrypt...');
617
+
618
+ try {
619
+
620
+ console.log('auto ', AutoEncrypt);
621
+ // Auto Encrypt wraps and manages the HTTPS server
622
+ // It expects the Express app and domain configuration
623
+ const autoEncrypt = new AutoEncrypt({
624
+ domains: [this.httpsConfig.domain],
625
+ server: this.fear.getApp()
626
+ });
627
+
628
+ if (this.httpsConfig.staging) {
629
+ autoEncrypt.staging = true;
630
+ }
631
+
632
+ if (this.autoEncryptOptions && this.autoEncryptOptions.settingsPath) {
633
+ autoEncrypt.settingsPath = this.autoEncryptOptions.settingsPath;
634
+ }
635
+
636
+ return new Promise((resolve, reject) => {
637
+ // Auto Encrypt's serve() method starts the server
638
+ autoEncrypt.serve(httpsPort)
639
+ .then((server) => {
640
+ httpsServer = server;
641
+ logger.info(`HTTPS server running on port ${httpsPort} with Auto Encrypt`);
642
+
643
+ // Setup HTTP redirect server if enabled
644
+ if (this.httpsConfig.redirectHttp) {
645
+ const httpRedirectPort = this.fear.getApp().get("PORT") || DEFAULT_PORT;
646
+ this.httpRedirectServer = this._createHttpRedirectServer(httpsPort);
647
+
648
+ this.httpRedirectServer.listen(httpRedirectPort, () => {
649
+ logger.info(`HTTP redirect server running on port ${httpRedirectPort} → HTTPS`);
650
+ });
651
+ }
652
+
653
+ resolve(server);
654
+ })
655
+ .catch((err) => {
656
+ logger.error(`Failed to start HTTPS server on port ${httpsPort}:`, err);
657
+ reject(err);
658
+ });
659
+ });
660
+ } catch (error) {
661
+ logger.error('Failed to create Auto Encrypt server:', error);
662
+ //throw error;
663
+ }
664
+
665
+ } else if (this.httpsConfig.mode === 'manual') {
666
+ // Manual certificate mode
667
+ logger.info('Starting HTTPS server with manual certificates...');
668
+
669
+ httpsServer = this._createManualHttpsServer();
670
+
671
+ return new Promise((resolve, reject) => {
672
+ httpsServer.listen(httpsPort, (err) => {
673
+ if (err) {
674
+ logger.error(`Failed to start HTTPS server on port ${httpsPort}:`, err);
675
+ return reject(err);
676
+ }
677
+
678
+ logger.info(`HTTPS server running on port ${httpsPort}`);
679
+ resolve(httpsServer);
680
+ });
681
+
682
+ // Setup HTTP redirect server if enabled
683
+ if (this.httpsConfig.redirectHttp) {
684
+ const httpRedirectPort = this.fear.getApp().get("PORT") || DEFAULT_PORT;
685
+ this.httpRedirectServer = this._createHttpRedirectServer(httpsPort);
686
+
687
+ this.httpRedirectServer.listen(httpRedirectPort, () => {
688
+ logger.info(`HTTP redirect server running on port ${httpRedirectPort} → HTTPS`);
689
+ });
690
+ }
691
+
692
+ httpsServer.on('error', (err) => {
693
+ if (err.code === 'EADDRINUSE') {
694
+ logger.error(`Port ${httpsPort} is already in use`);
695
+ } else {
696
+ logger.error('HTTPS server error:', err);
697
+ }
698
+ reject(err);
699
+ });
700
+ });
701
+ }
702
+ },
703
+
353
704
  /**
354
705
  * Perform graceful shutdown
355
706
  */
@@ -368,12 +719,22 @@ const FearServer = (function () {
368
719
  process.exit(1);
369
720
  }, SHUTDOWN_TIMEOUT);
370
721
 
371
- // Close server connections
372
- const closeServerPromise = this.server
373
- ? new Promise((resolve) => this.server.close(resolve))
374
- : Promise.resolve();
722
+ // Close all server connections
723
+ const closePromises = [];
724
+
725
+ if (this.server) {
726
+ closePromises.push(new Promise((resolve) => this.server.close(resolve)));
727
+ }
728
+
729
+ if (this.httpsServer) {
730
+ closePromises.push(new Promise((resolve) => this.httpsServer.close(resolve)));
731
+ }
732
+
733
+ if (this.httpRedirectServer) {
734
+ closePromises.push(new Promise((resolve) => this.httpRedirectServer.close(resolve)));
735
+ }
375
736
 
376
- return closeServerPromise
737
+ return Promise.all(closePromises)
377
738
  .then(() => {
378
739
  // Shutdown FEAR application
379
740
  if (this.fear && typeof this.fear.shutdown === 'function') {
@@ -443,7 +804,7 @@ const FearServer = (function () {
443
804
  },
444
805
 
445
806
  /**
446
- * Start the server
807
+ * Start the server (HTTP or HTTPS based on configuration)
447
808
  */
448
809
  startServer() {
449
810
  const port = this.fear.getApp().get("PORT") || DEFAULT_PORT;
@@ -460,20 +821,57 @@ const FearServer = (function () {
460
821
  // Initialize database connection
461
822
  return this.initializeDatabase()
462
823
  .then(() => {
463
- // Start the HTTP server
464
- return this.startHttpServer(port);
824
+ // Start HTTPS if configured, otherwise HTTP
825
+ if (this.httpsConfig) {
826
+ return this._startHttpsServer();
827
+ } else {
828
+ return this.startHttpServer(port);
829
+ }
465
830
  })
466
831
  .then((server) => {
467
- this.server = server;
832
+ if (this.httpsConfig) {
833
+ this.httpsServer = server;
834
+ } else {
835
+ this.server = server;
836
+ }
837
+
468
838
  logger.info('═══════════════════════════════════════');
469
- logger.info(`FEAR API Server Running on Port ${port}`);
839
+
840
+ if (this.httpsConfig) {
841
+ logger.info(`🔒 FEAR API Server Running on HTTPS Port ${this.httpsConfig.httpsPort}`);
842
+
843
+ if (this.httpsConfig.mode === 'greenlock') {
844
+ logger.info(`🔐 Auto-encryption: ENABLED (Greenlock/Let's Encrypt)`);
845
+ logger.info(`📧 Domain: ${this.httpsConfig.domain}`);
846
+ logger.info(`📧 Email: ${this.httpsConfig.email}`);
847
+ } else if (this.httpsConfig.mode === 'auto-encrypt') {
848
+ logger.info(`🔐 Auto-encryption: ENABLED (Auto Encrypt/Let's Encrypt)`);
849
+ logger.info(`📧 Domain: ${this.httpsConfig.domain}`);
850
+ } else {
851
+ logger.info(`🔐 Manual SSL certificates loaded`);
852
+ }
853
+
854
+ if (this.httpsConfig.redirectHttp) {
855
+ logger.info(`↪️ HTTP → HTTPS redirect enabled`);
856
+ }
857
+ } else {
858
+ logger.info(`FEAR API Server Running on HTTP Port ${port}`);
859
+ }
860
+
470
861
  logger.info('═══════════════════════════════════════');
471
862
 
472
863
  // Display registered React apps
473
864
  if (this.reactApps.length > 0) {
474
865
  logger.info('📱 React Apps:');
866
+ const protocol = this.httpsConfig ? 'https' : 'http';
867
+ const displayPort = this.httpsConfig ? this.httpsConfig.httpsPort : port;
868
+ const portDisplay = (protocol === 'https' && displayPort === 443) ||
869
+ (protocol === 'http' && displayPort === 80)
870
+ ? '' : `:${displayPort}`;
871
+
475
872
  this.reactApps.forEach(app => {
476
- logger.info(`• http://localhost:${port}${app.basePath}`);
873
+ const host = this.httpsConfig?.domain || 'localhost';
874
+ logger.info(`• ${protocol}://${host}${portDisplay}${app.basePath}`);
477
875
  });
478
876
  logger.info('═══════════════════════════════════════');
479
877
  }
@@ -507,6 +905,20 @@ const FearServer = (function () {
507
905
  return this.server;
508
906
  },
509
907
 
908
+ /**
909
+ * Get HTTPS server instance
910
+ */
911
+ getHttpsServer() {
912
+ return this.httpsServer;
913
+ },
914
+
915
+ /**
916
+ * Get HTTP redirect server instance
917
+ */
918
+ getHttpRedirectServer() {
919
+ return this.httpRedirectServer;
920
+ },
921
+
510
922
  /**
511
923
  * Get Router instance
512
924
  */
@@ -540,6 +952,20 @@ const FearServer = (function () {
540
952
  */
541
953
  isEnvLoaded() {
542
954
  return this.envLoaded;
955
+ },
956
+
957
+ /**
958
+ * Get HTTPS configuration
959
+ */
960
+ getHttpsConfig() {
961
+ return this.httpsConfig;
962
+ },
963
+
964
+ /**
965
+ * Check if HTTPS is enabled
966
+ */
967
+ isHttpsEnabled() {
968
+ return !!this.httpsConfig;
543
969
  }
544
970
  };
545
971
 
@@ -2,45 +2,32 @@ const { tryCatch } = require("../libs/handler/error");
2
2
  const methods = require("./crud");
3
3
  const Review = require("../models/review");
4
4
  const Product = require("../models/product");
5
+
5
6
  /**
6
7
  * Add or update a product review
7
8
  * @param {Object} req - Express request object
8
9
  * @param {Object} res - Express response object
9
10
  */
10
- exports.review = tryCatch(async (req, res) => {
11
+ exports.add = async (req, res) => {
11
12
  const { username, email, rating, comment } = req.body;
12
- const productId = req.params.productId || req.body.productId;
13
- //const userId = req.user._id;
14
-
13
+ const productId = req.params.id || req.body.productId;
14
+
15
15
  // Validate required fields
16
16
  if (!rating || rating < 1 || rating > 5) {
17
- return res.status(400).json({
18
- success: false,
19
- message: "Rating must be between 1 and 5"
20
- });
17
+ return Promise.resolve(
18
+ res.status(400).json({
19
+ success: false,
20
+ message: "Rating must be between 1 and 5"
21
+ }));
21
22
  }
22
-
23
23
  if (!comment || comment.trim().length === 0) {
24
- return res.status(400).json({
25
- success: false,
26
- message: "Comment is required"
27
- });
24
+ return Promise.resolve(
25
+ res.status(400).json({
26
+ success: false,
27
+ message: "Comment is required"
28
+ }));
28
29
  }
29
-
30
- // Find product and check if it exists
31
- const product = await Product.findById(productId);
32
- if (!product) {
33
- return res.status(404).json({
34
- success: false,
35
- message: "Product not found"
36
- });
37
- }
38
-
39
- // Check if user already reviewed this product
40
- const existingReviewIndex = product.reviews.findIndex(
41
- (review) => review.user.toString() === userId.toString()
42
- );
43
-
30
+
44
31
  const reviewData = {
45
32
  productId: productId,
46
33
  username,
@@ -50,57 +37,50 @@ exports.review = tryCatch(async (req, res) => {
50
37
  createdAt: new Date()
51
38
  };
52
39
 
53
- if (existingReviewIndex !== -1) {
54
- // Update existing review
55
- product.reviews[existingReviewIndex] = {
56
- ...product.reviews[existingReviewIndex],
57
- ...reviewData,
58
- updatedAt: new Date()
59
- };
60
- } else {
61
- // Add new review
62
- product.reviews.push(reviewData);
63
- }
64
-
65
- // Recalculate average rating
66
- const totalRating = product.reviews.reduce((sum, review) => sum + review.rating, 0);
67
- product.rating = Number((totalRating / product.reviews.length).toFixed(1));
68
- product.totalReviews = product.reviews.length;
69
-
70
- // Save product
71
- const updatedProduct = await product.save();
72
-
73
- // Also save to Review collection if it exists
74
- try {
75
- if (existingReviewIndex !== -1) {
76
- // Update existing review in Review collection
77
- await Review.findOneAndUpdate(
78
- { username, productId },
79
- reviewData,
80
- { upsert: true, new: true }
81
- );
82
- } else {
83
- // Create new review in Review collection
84
- await Review.create(reviewData);
85
- }
86
- } catch (reviewError) {
87
- console.warn('Failed to sync review to Review collection:', reviewError.message);
88
- }
89
-
90
- return res.status(200).json({
91
- success: true,
92
- message: existingReviewIndex !== -1 ? "Review updated successfully" : "Review added successfully",
93
- result: updatedProduct,
94
- review: reviewData
95
- });
96
- });
40
+ // Find product and check if it exists
41
+ return Product.findById(productId)
42
+ .then(product => {
43
+ if (!product) {
44
+ res.status(400).json({
45
+ success: false,
46
+ message: "Product not found"
47
+ });
48
+ return Promise.reject(new Error("Product not found"));
49
+ }
50
+
51
+ product.reviews.push(reviewData);
52
+
53
+ // Recalculate average rating
54
+ const totalRating = product.reviews.reduce((sum, review) => sum + review.rating, 0);
55
+ product.rating = Number((totalRating / product.reviews.length).toFixed(1));
56
+ product.totalReviews = product.reviews.length;
57
+
58
+ return product.save();
59
+ })
60
+ .then(() => {
61
+
62
+ return Review.create(reviewData);
63
+ })
64
+ .then(review => {
65
+ return res.status(200).json({
66
+ success: true,
67
+ message: "Review added successfully",
68
+ result: review,
69
+ });
70
+ //return Promise.resolve(review);
71
+ })
72
+ .catch(error => {
73
+ console.error('Error in review process:', error);
74
+ return Promise.reject(error);
75
+ });
76
+ };
97
77
 
98
78
  /**
99
79
  * Add or update product rating (alternative rating system)
100
80
  * @param {Object} req - Express request object
101
81
  * @param {Object} res - Express response object
102
82
  */
103
- exports.rating = tryCatch(async (req, res) => {
83
+ exports.rating = tryCatch((req, res) => {
104
84
  const { _id: userId } = req.user;
105
85
  const { star, prodId, comment = "" } = req.body;
106
86
 
@@ -120,64 +100,79 @@ exports.rating = tryCatch(async (req, res) => {
120
100
  }
121
101
 
122
102
  // Find product
123
- const product = await Product.findById(prodId);
124
- if (!product) {
125
- return res.status(404).json({
126
- success: false,
127
- message: "Product not found"
128
- });
129
- }
130
-
131
- // Initialize ratings array if it doesn't exist
132
- if (!product.ratings) {
133
- product.ratings = [];
134
- }
135
-
136
- // Check if user already rated this product
137
- const existingRatingIndex = product.ratings.findIndex(
138
- (rating) => rating.postedby.toString() === userId.toString()
139
- );
140
-
141
- const ratingData = {
142
- star: Number(star),
143
- comment: comment.trim(),
144
- postedby: userId,
145
- createdAt: new Date()
146
- };
147
-
148
- if (existingRatingIndex !== -1) {
149
- // Update existing rating
150
- product.ratings[existingRatingIndex] = {
151
- ...product.ratings[existingRatingIndex],
152
- ...ratingData,
153
- updatedAt: new Date()
154
- };
155
- } else {
156
- // Add new rating
157
- product.ratings.push(ratingData);
158
- }
103
+ Product.findById(prodId)
104
+ .then(product => {
105
+ if (!product) {
106
+ return res.status(404).json({
107
+ success: false,
108
+ message: "Product not found"
109
+ });
110
+ }
111
+
112
+ // Initialize ratings array if it doesn't exist
113
+ if (!product.ratings) {
114
+ product.ratings = [];
115
+ }
116
+
117
+ // Check if user already rated this product
118
+ const existingRatingIndex = product.ratings.findIndex(
119
+ (rating) => rating.postedby.toString() === userId.toString()
120
+ );
159
121
 
160
- // Calculate average rating
161
- const totalRatingSum = product.ratings.reduce((sum, rating) => sum + rating.star, 0);
162
- const averageRating = totalRatingSum / product.ratings.length;
163
-
164
- // Update product with new rating
165
- product.totalrating = Number(averageRating.toFixed(1));
166
- product.totalRatings = product.ratings.length;
167
-
168
- // Save updated product
169
- const updatedProduct = await product.save();
170
-
171
- return res.status(200).json({
172
- success: true,
173
- message: existingRatingIndex !== -1 ? "Rating updated successfully" : "Rating added successfully",
174
- result: updatedProduct,
175
- rating: {
176
- averageRating: product.totalrating,
177
- totalRatings: product.totalRatings,
178
- userRating: ratingData
179
- }
180
- });
122
+ const ratingData = {
123
+ star: Number(star),
124
+ comment: comment.trim(),
125
+ postedby: userId,
126
+ createdAt: new Date()
127
+ };
128
+
129
+ if (existingRatingIndex !== -1) {
130
+ // Update existing rating
131
+ product.ratings[existingRatingIndex] = {
132
+ ...product.ratings[existingRatingIndex],
133
+ ...ratingData,
134
+ updatedAt: new Date()
135
+ };
136
+ } else {
137
+ // Add new rating
138
+ product.ratings.push(ratingData);
139
+ }
140
+
141
+ // Calculate average rating
142
+ const totalRatingSum = product.ratings.reduce((sum, rating) => sum + rating.star, 0);
143
+ const averageRating = totalRatingSum / product.ratings.length;
144
+
145
+ // Update product with new rating
146
+ product.totalrating = Number(averageRating.toFixed(1));
147
+ product.totalRatings = product.ratings.length;
148
+
149
+ // Save updated product and return the promise
150
+ return product.save().then(updatedProduct => ({
151
+ updatedProduct,
152
+ existingRatingIndex,
153
+ ratingData
154
+ }));
155
+ })
156
+ .then(({ updatedProduct, existingRatingIndex, ratingData }) => {
157
+ return res.status(200).json({
158
+ success: true,
159
+ message: existingRatingIndex !== -1 ? "Rating updated successfully" : "Rating added successfully",
160
+ result: updatedProduct,
161
+ rating: {
162
+ averageRating: updatedProduct.totalrating,
163
+ totalRatings: updatedProduct.totalRatings,
164
+ userRating: ratingData
165
+ }
166
+ });
167
+ })
168
+ .catch(error => {
169
+ console.error('Error in rating process:', error);
170
+ return res.status(500).json({
171
+ success: false,
172
+ message: "Failed to process rating",
173
+ error: error.message
174
+ });
175
+ });
181
176
  });
182
177
 
183
178
  /**
@@ -185,19 +180,10 @@ exports.rating = tryCatch(async (req, res) => {
185
180
  * @param {Object} req - Express request object
186
181
  * @param {Object} res - Express response object
187
182
  */
188
- exports.getProductReviews = tryCatch(async (req, res) => {
189
- const productId = req.params.id;
183
+ exports.getProductReviews = tryCatch((req, res) => {
184
+ const productId = req.params.id || req.body.productId;
190
185
  const { page = 1, limit = 10, sortBy = 'newest' } = req.query;
191
186
 
192
- // Validate product exists
193
- const product = await Product.findById(productId);
194
- if (!product) {
195
- return res.status(404).json({
196
- success: false,
197
- message: "Product not found"
198
- });
199
- }
200
-
201
187
  let sortCriteria;
202
188
  switch (sortBy) {
203
189
  case 'oldest':
@@ -215,28 +201,39 @@ exports.getProductReviews = tryCatch(async (req, res) => {
215
201
  break;
216
202
  }
217
203
 
218
- const reviews = await Review.find({ product: productId })
219
- .populate('user', 'name email avatar')
220
- .sort(sortCriteria)
221
- .limit(Number(limit))
222
- .skip((Number(page) - 1) * Number(limit));
223
-
224
- const totalReviews = await Review.countDocuments({ product: productId });
225
- const totalPages = Math.ceil(totalReviews / Number(limit));
226
-
227
- return res.status(200).json({
228
- success: true,
229
- message: "Product reviews retrieved successfully",
230
- result: reviews,
231
- pagination: {
232
- currentPage: Number(page),
233
- totalPages,
234
- totalReviews,
235
- reviewsPerPage: Number(limit),
236
- hasNextPage: Number(page) < totalPages,
237
- hasPrevPage: Number(page) > 1
238
- }
239
- });
204
+ // Validate product exists
205
+ return Product.findById(productId)
206
+ .then(product => {
207
+ if (!product) {
208
+ return res.status(404).json({
209
+ success: false,
210
+ message: "Product not found"
211
+ });
212
+ }
213
+
214
+ const totalReviews = product.reviews.length;
215
+ const totalPages = Math.ceil(totalReviews / Number(limit));
216
+
217
+ return res.status(200).json({
218
+ success: true,
219
+ message: "Product reviews retrieved successfully",
220
+ result: product.reviews,
221
+ pagination: {
222
+ currentPage: Number(page),
223
+ totalPages,
224
+ totalReviews,
225
+ limit: Number(limit)
226
+ }
227
+ });
228
+ })
229
+ .catch(error => {
230
+ console.error('Error fetching reviews:', error);
231
+ return res.status(500).json({
232
+ success: false,
233
+ message: "Failed to fetch reviews",
234
+ error: error.message
235
+ });
236
+ });
240
237
  });
241
238
 
242
239
  // Extend with CRUD methods
@@ -245,4 +242,4 @@ for (const prop in crud) {
245
242
  if (crud.hasOwnProperty(prop)) {
246
243
  module.exports[prop] = crud[prop];
247
244
  }
248
- }
245
+ }
@@ -21,7 +21,8 @@ module.exports = function (fear) {
21
21
  const { apiKey, domain, region } = _this.mailConfig.mailgun;
22
22
 
23
23
  if (!apiKey || !domain) {
24
- throw new Error('Mailgun requires apiKey and domain in configuration.');
24
+ logger.error('Mailgun requires apiKey and domain in configuration.')
25
+ return;
25
26
  }
26
27
 
27
28
  const mailgun = new Mailgun(_this.mailConfig.mailgun);
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Greenlock Domain Configuration Script
5
+ *
6
+ * This script configures your domain(s) with Greenlock.
7
+ * Run this once before starting your server.
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ // Load environment variables
14
+ require('dotenv').config();
15
+
16
+ const domain = process.env.DOMAIN || process.argv[2];
17
+ const email = process.env.ADMIN_EMAIL || process.argv[3];
18
+ const altDomains = process.env.ALT_DOMAINS ? process.env.ALT_DOMAINS.split(',') : [];
19
+
20
+ if (!domain) {
21
+ console.error('❌ Error: Domain is required');
22
+ console.log('Usage: node configure-greenlock.js <domain> [email]');
23
+ console.log(' OR: Set DOMAIN and ADMIN_EMAIL in .env file');
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!email) {
28
+ console.error('❌ Error: Email is required');
29
+ console.log('Usage: node configure-greenlock.js <domain> <email>');
30
+ console.log(' OR: Set DOMAIN and ADMIN_EMAIL in .env file');
31
+ process.exit(1);
32
+ }
33
+
34
+ const allDomains = [domain, ...altDomains.filter(d => d && d !== domain)];
35
+
36
+ console.log('🔧 Configuring Greenlock...\n');
37
+ console.log(`Domain: ${domain}`);
38
+ console.log(`Email: ${email}`);
39
+ if (altDomains.length > 0) {
40
+ console.log(`Alt domains: ${altDomains.join(', ')}`);
41
+ }
42
+ console.log('');
43
+
44
+ try {
45
+ const greenlockExpress = require('@root/greenlock-express');
46
+
47
+ const configDir = process.env.GREENLOCK_DIR || path.join(__dirname, 'greenlock.d');
48
+
49
+ // Ensure config directory exists
50
+ if (!fs.existsSync(configDir)) {
51
+ fs.mkdirSync(configDir, { recursive: true });
52
+ console.log(`✓ Created config directory: ${configDir}`);
53
+ }
54
+
55
+ // Initialize Greenlock
56
+ const greenlock = greenlockExpress.init({
57
+ packageRoot: path.resolve(),
58
+ configDir: configDir,
59
+ maintainerEmail: email,
60
+ cluster: false
61
+ });
62
+
63
+ console.log('✓ Greenlock initialized\n', greenlock);
64
+
65
+ // Wait for Greenlock to be ready, then configure domains
66
+ console.log('⏳ Configuring domain...');
67
+
68
+ greenlock.ready((manager) => {
69
+ console.log('manager = ', manager);
70
+ manager.defaults({
71
+ agreeToTerms: true,
72
+ subscriberEmail: email
73
+ }).then(() => {
74
+ console.log('✓ Set default configuration');
75
+
76
+ return greenlock.manager.add({
77
+ subject: domain,
78
+ altnames: allDomains
79
+ });
80
+ }).then(() => {
81
+ console.log(`✓ Domain configured: ${domain}`);
82
+ if (altDomains.length > 0) {
83
+ console.log(`✓ Alt domains added: ${altDomains.join(', ')}`);
84
+ }
85
+ console.log('');
86
+ console.log('✅ Greenlock configuration complete!');
87
+ console.log('You can now start your server.');
88
+ process.exit(0);
89
+ }).catch((error) => {
90
+ console.error('❌ Configuration failed:', error.message);
91
+ console.log('');
92
+ console.log('Troubleshooting:');
93
+ console.log('1. Make sure @root/greenlock-express is installed');
94
+ console.log('2. Check that your domain points to this server');
95
+ console.log('3. Ensure ports 80 and 443 are accessible');
96
+ process.exit(1);
97
+ });})
98
+
99
+ } catch (error) {
100
+ console.error('❌ Error:', error.message);
101
+ console.log('');
102
+ console.log('Make sure Greenlock is installed:');
103
+ console.log(' npm install @root/greenlock-express');
104
+ process.exit(1);
105
+ }
package/models/review.js CHANGED
@@ -3,12 +3,13 @@ const mongoose = require("mongoose");
3
3
  const reviewSchema = new mongoose.Schema({
4
4
  productId: { type: mongoose.Schema.Types.ObjectId, ref:"Product", required:true },
5
5
  username: { type: String, required: true },
6
- email: { type: String, required: false, unique: true, lowercase: true, trim: true,
6
+ email: { type: String, required: false, lowercase: true, trim: true,
7
7
  match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
8
8
  },
9
- rating: { type:Number, required:true, min:1, max:5 },
10
- comment: { type:String, required:true },
11
- createdAt: { type:Date, default:Date.now }
9
+ rating: { type: Number, required:true, min:1, max:5 },
10
+ comment: { type: String, required:true },
11
+ createdAt: { type: Date, default:Date.now },
12
+ verified: { type: Boolean, default: false, index: true }
12
13
  },
13
14
  { timestamps: true, versionKey:false}
14
15
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feardread/fear",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/routes/review.js CHANGED
@@ -2,10 +2,14 @@ const Review = require("../controllers/review");
2
2
 
3
3
  module.exports = (fear) => {
4
4
  const router = fear.createRouter();
5
-
6
- router.post("/new", Review.review)
7
- .get("/rating", Review.rating)
8
- .get("/by-product/:productId", Review.getProductReviews);
9
- router.get("/by-product", Review.getProductReviews);
5
+ const handler = fear.getHandler();
6
+
7
+ router.get("/all", handler.async(Review.all));
8
+ router.post("/new", handler.async(Review.add));
9
+ router.route("/:id")
10
+ .put(Review.update)
11
+ .delete(Review.delete)
12
+ .get(Review.getProductReviews);
13
+
10
14
  return router;
11
15
  };