@feardread/fear 2.0.5 → 2.0.7

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 CHANGED
@@ -15,6 +15,15 @@ module.exports = FEAR = (() => {
15
15
  const DEFAULT_JSON_LIMIT = '10mb';
16
16
  const DEFAULT_ROUTE_PATH = '/fear/api';
17
17
  const AGENT_ROUTE_PATH = '/fear/api/agent';
18
+ const FEAR_LOGO=`
19
+ _________________________
20
+ | ___| ____| / \ | _ \
21
+ | |_ | _| / _ \ | |_) |
22
+ | _| | |___ / ___ \| _ <
23
+ |_| |_____/_/ \_\_| \_\
24
+ ----The Quieter we become----
25
+ -- the more we are able to hear.--
26
+ `
18
27
 
19
28
  // Constructor function
20
29
  const FEAR = function (config) {
@@ -47,7 +56,6 @@ module.exports = FEAR = (() => {
47
56
  this.setupRoutes();
48
57
  };
49
58
 
50
-
51
59
  FEAR.prototype = {
52
60
  constructor: FEAR,
53
61
  getStripe() {
@@ -114,7 +122,7 @@ module.exports = FEAR = (() => {
114
122
  this.db = require("./libs/db");
115
123
  this.handler = require("./libs/handler");
116
124
  this.validator = require("./libs/validator");
117
- this.logo = this.env.FEAR_LOGO;
125
+ this.logo = FEAR_LOGO;
118
126
  this.origins = this.getAllowedOrigins();
119
127
  },
120
128
 
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,192 @@ 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
+ _createHttpRedirectServer(httpsPort) {
282
+ const redirectApp = express();
283
+
284
+ redirectApp.use((req, res) => {
285
+ const host = req.headers.host.split(':')[0];
286
+ const httpsUrl = `https://${host}${httpsPort !== 443 ? ':' + httpsPort : ''}${req.url}`;
287
+ res.redirect(301, httpsUrl);
288
+ });
289
+
290
+ return http.createServer(redirectApp);
291
+ },
292
+
98
293
  /**
99
294
  * Validate that required files exist for React app
100
295
  * @param {string} buildPath - Path to build directory
@@ -239,7 +434,7 @@ const FearServer = (function () {
239
434
  };
240
435
 
241
436
  this.fear.getApp().use((req, res, next) => {
242
- res.header('Access-Control-Allow-Origin', defaultOptions.origin);
437
+ res.header('Access-Control-Allow-Origin', '*');
243
438
  res.header('Access-Control-Allow-Methods', defaultOptions.methods.join(', '));
244
439
  res.header('Access-Control-Allow-Headers', defaultOptions.allowedHeaders.join(', '));
245
440
 
@@ -259,34 +454,31 @@ const FearServer = (function () {
259
454
  },
260
455
 
261
456
  /**
262
- * Setup API routes prefix (useful to avoid conflicts with React routes)
263
- * @param {string} prefix - API prefix (e.g., '/api')
457
+ * Setup API prefix for all routes
458
+ * @param {string} prefix - API prefix (e.g., '/api/v1')
264
459
  */
265
- setupAPIPrefix(prefix = '/fear/api') {
460
+ setupAPIPrefix(prefix) {
461
+ if (!prefix || typeof prefix !== 'string') {
462
+ throw new Error('API prefix must be a string');
463
+ }
464
+
266
465
  const normalizedPrefix = `/${prefix.replace(/^\/+|\/+$/g, '')}`;
267
466
 
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;
467
+ this.fear.getLogger().info(`Setting up API prefix: ${normalizedPrefix}`);
468
+ // This would need to be implemented in your FEAR framework
469
+ // to prefix all routes - just documenting the intent here
276
470
  },
277
471
 
278
472
  /**
279
- * Setup process event handlers for graceful shutdown
473
+ * Setup process signal handlers for graceful shutdown
280
474
  */
281
475
  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
- });
476
+ // Prevent duplicate listeners
477
+ if (this._handlersSetup) {
478
+ return;
479
+ }
480
+ this._handlersSetup = true;
288
481
 
289
- // Handle uncaught exceptions
290
482
  process.on("uncaughtException", (err) => {
291
483
  this.fear.getLogger().error('Uncaught Exception:', err);
292
484
  this.gracefulShutdown('uncaughtException');
@@ -350,6 +542,161 @@ const FearServer = (function () {
350
542
  });
351
543
  },
352
544
 
545
+ /**
546
+ * Start HTTPS server
547
+ * @private
548
+ */
549
+ async _startHttpsServer() {
550
+ const logger = this.fear.getLogger();
551
+ const httpsPort = this.httpsConfig.httpsPort;
552
+
553
+ let httpsServer;
554
+
555
+ if (this.httpsConfig.mode === 'greenlock') {
556
+ // Greenlock Express initialization and startup
557
+ logger.info('Starting HTTPS server with Greenlock auto-encryption...');
558
+
559
+ const greenlockExpress = require('@root/greenlock-express');
560
+
561
+ return new Promise((resolve, reject) => {
562
+ try {
563
+ // Initialize Greenlock with full configuration
564
+ const greenlock = greenlockExpress.init({
565
+ packageRoot: this.greenlockConfig.packageRoot,
566
+ configDir: this.greenlockConfig.configDir,
567
+ maintainerEmail: this.greenlockConfig.maintainerEmail,
568
+ cluster: false,
569
+
570
+ // Notify callback for errors
571
+ notify: (event, details) => {
572
+ if (event === 'error') {
573
+ logger.error('Greenlock error:', details);
574
+ } else if (event === 'warning') {
575
+ logger.warn('Greenlock warning:', details);
576
+ }
577
+ }
578
+ });
579
+
580
+ // Store greenlock instance
581
+ this.greenlock = greenlock;
582
+
583
+ // Serve the app - Greenlock handles everything automatically
584
+ // It will listen on port 80 (HTTP) and 443 (HTTPS)
585
+ greenlock.serve(this.fear.getApp());
586
+
587
+ logger.info(`✓ Greenlock HTTPS server started`);
588
+ logger.info(`✓ Listening on port 443 (HTTPS)`);
589
+ logger.info(`✓ Listening on port 80 (HTTP → HTTPS redirect)`);
590
+ logger.info(`✓ Staging mode: ${this.greenlockConfig.staging ? 'ENABLED' : 'DISABLED'}`);
591
+ logger.info('');
592
+ logger.info('⚠️ IMPORTANT: Configure your domain using Greenlock CLI:');
593
+ logger.info(` npx greenlock add --subject ${this.greenlockConfig.domain} --altnames ${this.greenlockConfig.altnames.join(',')}`);
594
+ logger.info('');
595
+
596
+ // Mark server as started
597
+ httpsServer = {
598
+ greenlockManaged: true,
599
+ greenlock: greenlock
600
+ };
601
+
602
+ resolve(httpsServer);
603
+
604
+ } catch (error) {
605
+ logger.error('Failed to initialize Greenlock:', error);
606
+ reject(error);
607
+ }
608
+ });
609
+
610
+ } else if (this.httpsConfig.mode === 'auto-encrypt') {
611
+ // Auto Encrypt mode
612
+ logger.info('Starting HTTPS server with Auto Encrypt...');
613
+
614
+ try {
615
+
616
+ console.log('auto ', AutoEncrypt);
617
+ // Auto Encrypt wraps and manages the HTTPS server
618
+ // It expects the Express app and domain configuration
619
+ const autoEncrypt = new AutoEncrypt({
620
+ domains: [this.httpsConfig.domain],
621
+ server: this.fear.getApp()
622
+ });
623
+
624
+ if (this.httpsConfig.staging) {
625
+ autoEncrypt.staging = true;
626
+ }
627
+
628
+ if (this.autoEncryptOptions && this.autoEncryptOptions.settingsPath) {
629
+ autoEncrypt.settingsPath = this.autoEncryptOptions.settingsPath;
630
+ }
631
+
632
+ return new Promise((resolve, reject) => {
633
+ // Auto Encrypt's serve() method starts the server
634
+ autoEncrypt.serve(httpsPort)
635
+ .then((server) => {
636
+ httpsServer = server;
637
+ logger.info(`HTTPS server running on port ${httpsPort} with Auto Encrypt`);
638
+
639
+ // Setup HTTP redirect server if enabled
640
+ if (this.httpsConfig.redirectHttp) {
641
+ const httpRedirectPort = this.fear.getApp().get("PORT") || DEFAULT_PORT;
642
+ this.httpRedirectServer = this._createHttpRedirectServer(httpsPort);
643
+
644
+ this.httpRedirectServer.listen(httpRedirectPort, () => {
645
+ logger.info(`HTTP redirect server running on port ${httpRedirectPort} → HTTPS`);
646
+ });
647
+ }
648
+
649
+ resolve(server);
650
+ })
651
+ .catch((err) => {
652
+ logger.error(`Failed to start HTTPS server on port ${httpsPort}:`, err);
653
+ reject(err);
654
+ });
655
+ });
656
+ } catch (error) {
657
+ logger.error('Failed to create Auto Encrypt server:', error);
658
+ //throw error;
659
+ }
660
+
661
+ } else if (this.httpsConfig.mode === 'manual') {
662
+ // Manual certificate mode
663
+ logger.info('Starting HTTPS server with manual certificates...');
664
+
665
+ httpsServer = this._createManualHttpsServer();
666
+
667
+ return new Promise((resolve, reject) => {
668
+ httpsServer.listen(httpsPort, (err) => {
669
+ if (err) {
670
+ logger.error(`Failed to start HTTPS server on port ${httpsPort}:`, err);
671
+ return reject(err);
672
+ }
673
+
674
+ logger.info(`HTTPS server running on port ${httpsPort}`);
675
+ resolve(httpsServer);
676
+ });
677
+
678
+ // Setup HTTP redirect server if enabled
679
+ if (this.httpsConfig.redirectHttp) {
680
+ const httpRedirectPort = this.fear.getApp().get("PORT") || DEFAULT_PORT;
681
+ this.httpRedirectServer = this._createHttpRedirectServer(httpsPort);
682
+
683
+ this.httpRedirectServer.listen(httpRedirectPort, () => {
684
+ logger.info(`HTTP redirect server running on port ${httpRedirectPort} → HTTPS`);
685
+ });
686
+ }
687
+
688
+ httpsServer.on('error', (err) => {
689
+ if (err.code === 'EADDRINUSE') {
690
+ logger.error(`Port ${httpsPort} is already in use`);
691
+ } else {
692
+ logger.error('HTTPS server error:', err);
693
+ }
694
+ reject(err);
695
+ });
696
+ });
697
+ }
698
+ },
699
+
353
700
  /**
354
701
  * Perform graceful shutdown
355
702
  */
@@ -368,12 +715,22 @@ const FearServer = (function () {
368
715
  process.exit(1);
369
716
  }, SHUTDOWN_TIMEOUT);
370
717
 
371
- // Close server connections
372
- const closeServerPromise = this.server
373
- ? new Promise((resolve) => this.server.close(resolve))
374
- : Promise.resolve();
718
+ // Close all server connections
719
+ const closePromises = [];
720
+
721
+ if (this.server) {
722
+ closePromises.push(new Promise((resolve) => this.server.close(resolve)));
723
+ }
724
+
725
+ if (this.httpsServer) {
726
+ closePromises.push(new Promise((resolve) => this.httpsServer.close(resolve)));
727
+ }
728
+
729
+ if (this.httpRedirectServer) {
730
+ closePromises.push(new Promise((resolve) => this.httpRedirectServer.close(resolve)));
731
+ }
375
732
 
376
- return closeServerPromise
733
+ return Promise.all(closePromises)
377
734
  .then(() => {
378
735
  // Shutdown FEAR application
379
736
  if (this.fear && typeof this.fear.shutdown === 'function') {
@@ -443,7 +800,7 @@ const FearServer = (function () {
443
800
  },
444
801
 
445
802
  /**
446
- * Start the server
803
+ * Start the server (HTTP or HTTPS based on configuration)
447
804
  */
448
805
  startServer() {
449
806
  const port = this.fear.getApp().get("PORT") || DEFAULT_PORT;
@@ -460,20 +817,57 @@ const FearServer = (function () {
460
817
  // Initialize database connection
461
818
  return this.initializeDatabase()
462
819
  .then(() => {
463
- // Start the HTTP server
464
- return this.startHttpServer(port);
820
+ // Start HTTPS if configured, otherwise HTTP
821
+ if (this.httpsConfig) {
822
+ return this._startHttpsServer();
823
+ } else {
824
+ return this.startHttpServer(port);
825
+ }
465
826
  })
466
827
  .then((server) => {
467
- this.server = server;
828
+ if (this.httpsConfig) {
829
+ this.httpsServer = server;
830
+ } else {
831
+ this.server = server;
832
+ }
833
+
468
834
  logger.info('═══════════════════════════════════════');
469
- logger.info(`FEAR API Server Running on Port ${port}`);
835
+
836
+ if (this.httpsConfig) {
837
+ logger.info(`🔒 FEAR API Server Running on HTTPS Port ${this.httpsConfig.httpsPort}`);
838
+
839
+ if (this.httpsConfig.mode === 'greenlock') {
840
+ logger.info(`🔐 Auto-encryption: ENABLED (Greenlock/Let's Encrypt)`);
841
+ logger.info(`📧 Domain: ${this.httpsConfig.domain}`);
842
+ logger.info(`📧 Email: ${this.httpsConfig.email}`);
843
+ } else if (this.httpsConfig.mode === 'auto-encrypt') {
844
+ logger.info(`🔐 Auto-encryption: ENABLED (Auto Encrypt/Let's Encrypt)`);
845
+ logger.info(`📧 Domain: ${this.httpsConfig.domain}`);
846
+ } else {
847
+ logger.info(`🔐 Manual SSL certificates loaded`);
848
+ }
849
+
850
+ if (this.httpsConfig.redirectHttp) {
851
+ logger.info(`↪️ HTTP → HTTPS redirect enabled`);
852
+ }
853
+ } else {
854
+ logger.info(`FEAR API Server Running on HTTP Port ${port}`);
855
+ }
856
+
470
857
  logger.info('═══════════════════════════════════════');
471
858
 
472
859
  // Display registered React apps
473
860
  if (this.reactApps.length > 0) {
474
861
  logger.info('📱 React Apps:');
862
+ const protocol = this.httpsConfig ? 'https' : 'http';
863
+ const displayPort = this.httpsConfig ? this.httpsConfig.httpsPort : port;
864
+ const portDisplay = (protocol === 'https' && displayPort === 443) ||
865
+ (protocol === 'http' && displayPort === 80)
866
+ ? '' : `:${displayPort}`;
867
+
475
868
  this.reactApps.forEach(app => {
476
- logger.info(`• http://localhost:${port}${app.basePath}`);
869
+ const host = this.httpsConfig?.domain || 'localhost';
870
+ logger.info(`• ${protocol}://${host}${portDisplay}${app.basePath}`);
477
871
  });
478
872
  logger.info('═══════════════════════════════════════');
479
873
  }
@@ -507,6 +901,20 @@ const FearServer = (function () {
507
901
  return this.server;
508
902
  },
509
903
 
904
+ /**
905
+ * Get HTTPS server instance
906
+ */
907
+ getHttpsServer() {
908
+ return this.httpsServer;
909
+ },
910
+
911
+ /**
912
+ * Get HTTP redirect server instance
913
+ */
914
+ getHttpRedirectServer() {
915
+ return this.httpRedirectServer;
916
+ },
917
+
510
918
  /**
511
919
  * Get Router instance
512
920
  */
@@ -540,6 +948,20 @@ const FearServer = (function () {
540
948
  */
541
949
  isEnvLoaded() {
542
950
  return this.envLoaded;
951
+ },
952
+
953
+ /**
954
+ * Get HTTPS configuration
955
+ */
956
+ getHttpsConfig() {
957
+ return this.httpsConfig;
958
+ },
959
+
960
+ /**
961
+ * Check if HTTPS is enabled
962
+ */
963
+ isHttpsEnabled() {
964
+ return !!this.httpsConfig;
543
965
  }
544
966
  };
545
967
 
@@ -34,7 +34,7 @@ const sanitizeUpdateData = (data) => {
34
34
  */
35
35
  const processImages = (images) => {
36
36
  if (!images) return Promise.resolve(null);
37
-
37
+ console.log('processing images = ', images);
38
38
  const imageArray = Array.isArray(images)
39
39
  ? images
40
40
  : images.split(',').map(item => item.trim());
@@ -132,34 +132,34 @@ exports.read = tryCatch((Model, req, res) => {
132
132
  */
133
133
  exports.create = tryCatch((Model, req, res) => {
134
134
  const documentData = { ...req.body };
135
+ const featured = documentData.featuredImage;
136
+ if (featured && documentData.images) documentData.images.push(featured);
137
+ let imagePromise;
138
+
139
+ if (documentData.images && documentData.images.length !== 0) {
140
+ imagePromise = processImages(documentData.images);
141
+ } else {
142
+ imagePromise = Promise.resolve(null);
143
+ }
135
144
 
136
- return processImages(documentData.images)
137
- .then(imageLinks => {
145
+ return imagePromise
146
+ .then((imageLinks) => {
138
147
  if (imageLinks) {
139
148
  documentData.images = imageLinks;
140
149
  }
141
-
142
- console.log('Creating document:', documentData);
143
- const document = new Model(documentData);
144
-
150
+
151
+ const document = new Model(documentData);
145
152
  return document.save();
146
- })
147
- .then(result => {
148
- return res.status(201).json({
149
- result,
150
- success: true,
151
- message: `Document created successfully in ${Model.modelName} collection`
153
+ })
154
+ .then((result) => {
155
+ return res.status(201)
156
+ .json({ result, success: true, message: `Document created successfully in ${Model.modelName} collection`
152
157
  });
153
- })
154
- .catch(error => {
158
+ })
159
+ .catch((error) => {
155
160
  console.error('Error creating document:', error);
156
- return res.status(500).json({
157
- result: null,
158
- success: false,
159
- message: "Error creating document",
160
- error: error.message
161
- });
162
- });
161
+ return res.status(500).json({ result: null, success: false, message: "Error creating document", error: error.message });
162
+ })
163
163
  });
164
164
 
165
165
  /**
@@ -26,21 +26,21 @@ const convertToBase64 = (file) => {
26
26
  * @param {string} file - Base64 encoded file or file path
27
27
  * @returns {Promise<Object>} Avatar object with public_id and url
28
28
  */
29
- const uploadAvatar = async (file) => {
30
- try {
31
- const result = await cloudinary.uploader.upload(file, {
32
- folder: "avatar",
33
- width: 150,
34
- crop: "scale",
29
+ const uploadAvatar = (file) => {
30
+ return cloudinary.uploader.upload(file, {
31
+ folder: "avatar",
32
+ width: 150,
33
+ crop: "scale",
34
+ })
35
+ .then((result) => {
36
+ return {
37
+ public_id: result.public_id,
38
+ url: result.secure_url,
39
+ };
40
+ })
41
+ .catch((error) => {
42
+ throw new Error(`Avatar upload failed: ${error.message}`);
35
43
  });
36
-
37
- return {
38
- public_id: result.public_id,
39
- url: result.secure_url,
40
- };
41
- } catch (error) {
42
- throw new Error(`Avatar upload failed: ${error.message}`);
43
- }
44
44
  };
45
45
 
46
46
  /**
@@ -49,41 +49,49 @@ const uploadAvatar = async (file) => {
49
49
  * @param {number} chunkSize - Number of files to upload concurrently
50
50
  * @returns {Promise<Object[]>} Array of image objects with public_id and url
51
51
  */
52
- const uploadImages = async (files, chunkSize = 3) => {
53
- try {
54
- // Normalize input to array
55
- const imageArray = Array.isArray(files) ? [...files] : [files];
56
-
57
- if (imageArray.length === 0) {
58
- return [];
59
- }
52
+ const uploadImages = (files, chunkSize = 3) => {
53
+ // Normalize input to array
54
+ const imageArray = Array.isArray(files) ? [...files] : [files];
55
+
56
+ if (imageArray.length === 0) {
57
+ return Promise.resolve([]);
58
+ }
60
59
 
61
- const imageLinks = [];
60
+ const imageLinks = [];
61
+
62
+ // Create a promise chain for sequential chunk processing
63
+ let promiseChain = Promise.resolve();
64
+
65
+ // Process images in chunks to avoid overwhelming the API
66
+ for (let i = 0; i < imageArray.length; i += chunkSize) {
67
+ const chunk = imageArray.slice(i, i + chunkSize);
62
68
 
63
- // Process images in chunks to avoid overwhelming the API
64
- for (let i = 0; i < imageArray.length; i += chunkSize) {
65
- const chunk = imageArray.slice(i, i + chunkSize);
66
-
69
+ promiseChain = promiseChain.then(() => {
67
70
  const uploadPromises = chunk.map((image) =>
68
71
  cloudinary.uploader.upload(image, {
69
72
  folder: "products",
70
73
  })
71
74
  );
72
75
 
73
- const results = await Promise.all(uploadPromises);
74
-
75
- const chunkResults = results.map((result) => ({
76
- product_id: result.public_id,
77
- url: result.secure_url,
78
- }));
79
-
80
- imageLinks.push(...chunkResults);
81
- }
82
-
83
- return imageLinks;
84
- } catch (error) {
85
- throw new Error(`Image upload failed: ${error}`);
76
+ return Promise.all(uploadPromises)
77
+ .then((results) => {
78
+ const chunkResults = results.map((result) => ({
79
+ public_id: result.public_id,
80
+ url: result.secure_url,
81
+ }));
82
+
83
+ imageLinks.push(...chunkResults);
84
+ });
85
+ });
86
86
  }
87
+
88
+ return promiseChain
89
+ .then(() => {
90
+ return imageLinks;
91
+ })
92
+ .catch((error) => {
93
+ throw new Error('Image upload failed: ', error);
94
+ });
87
95
  };
88
96
 
89
97
  /**
@@ -130,56 +138,55 @@ const uploadPhoto = multer({
130
138
  * @param {Object} res - Express response object
131
139
  * @param {Function} next - Express next function
132
140
  */
133
- const resizeImages = async (req, res, next) => {
134
- try {
135
- // Skip if no files uploaded or no directory specified
136
- if (!req.files || !req.directory) {
137
- return next();
138
- }
139
-
140
- // Ensure directory exists
141
- const targetDir = path.join("public/images", req.directory);
142
- if (!fs.existsSync(targetDir)) {
143
- fs.mkdirSync(targetDir, { recursive: true });
144
- }
145
-
146
- // Process all files concurrently
147
- await Promise.all(
148
- req.files.map(async (file) => {
149
- const originalPath = file.path;
150
- const targetPath = path.join(targetDir, file.filename);
151
-
152
- try {
153
- // Resize and convert image
154
- await sharp(originalPath)
155
- .resize(300, 300, {
156
- fit: 'cover',
157
- position: 'center'
158
- })
159
- .jpeg({ quality: 90 })
160
- .toFile(targetPath);
161
-
162
- // Clean up original file
163
- if (fs.existsSync(originalPath)) {
164
- fs.unlinkSync(originalPath);
165
- }
166
- } catch (imageError) {
167
- console.error(`Failed to process image ${file.filename}:`, imageError);
168
- // Clean up files on error
169
- [originalPath, targetPath].forEach(filePath => {
170
- if (fs.existsSync(filePath)) {
171
- fs.unlinkSync(filePath);
172
- }
173
- });
174
- throw imageError;
141
+ const resizeImages = (req, res, next) => {
142
+ // Skip if no files uploaded or no directory specified
143
+ if (!req.files || !req.directory) {
144
+ return next();
145
+ }
146
+
147
+ // Ensure directory exists
148
+ const targetDir = path.join("public/images", req.directory);
149
+ if (!fs.existsSync(targetDir)) {
150
+ fs.mkdirSync(targetDir, { recursive: true });
151
+ }
152
+
153
+ // Process all files concurrently
154
+ const processPromises = req.files.map((file) => {
155
+ const originalPath = file.path;
156
+ const targetPath = path.join(targetDir, file.filename);
157
+
158
+ return sharp(originalPath)
159
+ .resize(300, 300, {
160
+ fit: 'cover',
161
+ position: 'center'
162
+ })
163
+ .jpeg({ quality: 90 })
164
+ .toFile(targetPath)
165
+ .then(() => {
166
+ // Clean up original file
167
+ if (fs.existsSync(originalPath)) {
168
+ fs.unlinkSync(originalPath);
175
169
  }
176
170
  })
177
- );
171
+ .catch((imageError) => {
172
+ console.error(`Failed to process image ${file.filename}:`, imageError);
173
+ // Clean up files on error
174
+ [originalPath, targetPath].forEach(filePath => {
175
+ if (fs.existsSync(filePath)) {
176
+ fs.unlinkSync(filePath);
177
+ }
178
+ });
179
+ throw imageError;
180
+ });
181
+ });
178
182
 
179
- next();
180
- } catch (error) {
181
- next(new Error(`Image resize failed: ${error.message}`));
182
- }
183
+ Promise.all(processPromises)
184
+ .then(() => {
185
+ next();
186
+ })
187
+ .catch((error) => {
188
+ next(new Error(`Image resize failed: ${error.message}`));
189
+ });
183
190
  };
184
191
 
185
192
  /**
@@ -187,13 +194,14 @@ const resizeImages = async (req, res, next) => {
187
194
  * @param {string} publicId - Public ID of the image to delete
188
195
  * @returns {Promise<Object>} Deletion result
189
196
  */
190
- const deleteImage = async (publicId) => {
191
- try {
192
- const result = await cloudinary.uploader.destroy(publicId);
193
- return result;
194
- } catch (error) {
195
- throw new Error(`Image deletion failed: ${error.message}`);
196
- }
197
+ const deleteImage = (publicId) => {
198
+ return cloudinary.uploader.destroy(publicId)
199
+ .then((result) => {
200
+ return result;
201
+ })
202
+ .catch((error) => {
203
+ throw new Error(`Image deletion failed: ${error.message}`);
204
+ });
197
205
  };
198
206
 
199
207
  /**
@@ -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.serveApp({
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/blog.js CHANGED
@@ -3,13 +3,9 @@ const slugify = require("slugify");
3
3
 
4
4
  const blogSchema = new mongoose.Schema(
5
5
  {
6
- // Basic Information
7
- title: {
8
- type: String,
6
+ title: {type: String, trim: true, index: true,
9
7
  required: [true, "Blog title is required"],
10
- trim: true,
11
- maxlength: [200, "Title cannot exceed 200 characters"],
12
- index: true
8
+ maxlength: [300, "Title cannot exceed 200 characters"],
13
9
  },
14
10
 
15
11
  slug: {
@@ -22,13 +18,13 @@ const blogSchema = new mongoose.Schema(
22
18
  subtitle: {
23
19
  type: String,
24
20
  trim: true,
25
- maxlength: [250, "Subtitle cannot exceed 250 characters"]
21
+ maxlength: [350, "Subtitle cannot exceed 250 characters"]
26
22
  },
27
23
 
28
24
  excerpt: {
29
25
  type: String,
30
26
  trim: true,
31
- maxlength: [500, "Excerpt cannot exceed 500 characters"]
27
+ maxlength: [600, "Excerpt cannot exceed 500 characters"]
32
28
  },
33
29
 
34
30
  content: {
@@ -43,10 +39,6 @@ const blogSchema = new mongoose.Schema(
43
39
  images: [{
44
40
  public_id: { type: String },
45
41
  url: { type: String },
46
- secure_url: { type: String },
47
- alt: { type: String },
48
- caption: { type: String },
49
- order: { type: Number, default: 0 }
50
42
  }],
51
43
 
52
44
  video: {
@@ -285,11 +277,11 @@ const blogSchema = new mongoose.Schema(
285
277
  seo: {
286
278
  metaTitle: {
287
279
  type: String,
288
- maxlength: [70, "Meta title cannot exceed 70 characters"]
280
+ maxlength: [170, "Meta title cannot exceed 170 characters"]
289
281
  },
290
282
  metaDescription: {
291
283
  type: String,
292
- maxlength: [160, "Meta description cannot exceed 160 characters"]
284
+ maxlength: [260, "Meta description cannot exceed 260 characters"]
293
285
  },
294
286
  metaKeywords: [{ type: String }],
295
287
  focusKeyword: { type: String },
package/models/brand.js CHANGED
@@ -43,7 +43,6 @@ brandSchema.index({ createdAt: -1 });
43
43
  // Text index for search
44
44
  brandSchema.index({
45
45
  name: "text",
46
- title: "text",
47
46
  tags: "text"
48
47
  });
49
48
 
@@ -83,11 +82,6 @@ brandSchema.pre("save", async function(next) {
83
82
  this.active = this.isActive;
84
83
  }
85
84
 
86
- // Set title to name if not provided
87
- if (!this.title) {
88
- this.title = this.name;
89
- }
90
-
91
85
  next();
92
86
  });
93
87
 
package/models/product.js CHANGED
@@ -19,7 +19,12 @@ const productSchema = new mongoose.Schema({
19
19
  url: String
20
20
  },
21
21
  ],
22
- tags: String,
22
+ tags: [{
23
+ type: String,
24
+ trim: true,
25
+ lowercase: true,
26
+ index: true
27
+ }],
23
28
  ratings: [{
24
29
  star: Number,
25
30
  comment: String,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feardread/fear",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {