@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 +10 -2
- package/FEARServer.js +453 -31
- package/controllers/crud/crud.js +22 -22
- package/libs/cloud/index.js +102 -94
- package/libs/greenlock/setup.js +105 -0
- package/models/blog.js +6 -14
- package/models/brand.js +0 -6
- package/models/product.js +6 -1
- package/package.json +1 -1
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 =
|
|
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',
|
|
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
|
|
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
|
|
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.
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
473
|
+
* Setup process signal handlers for graceful shutdown
|
|
280
474
|
*/
|
|
281
475
|
setupProcessHandlers() {
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
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
|
|
464
|
-
|
|
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.
|
|
828
|
+
if (this.httpsConfig) {
|
|
829
|
+
this.httpsServer = server;
|
|
830
|
+
} else {
|
|
831
|
+
this.server = server;
|
|
832
|
+
}
|
|
833
|
+
|
|
468
834
|
logger.info('═══════════════════════════════════════');
|
|
469
|
-
|
|
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
|
-
|
|
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
|
|
package/controllers/crud/crud.js
CHANGED
|
@@ -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
|
|
137
|
-
|
|
145
|
+
return imagePromise
|
|
146
|
+
.then((imageLinks) => {
|
|
138
147
|
if (imageLinks) {
|
|
139
148
|
documentData.images = imageLinks;
|
|
140
149
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const document = new Model(documentData);
|
|
144
|
-
|
|
150
|
+
|
|
151
|
+
const document = new Model(documentData);
|
|
145
152
|
return document.save();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return res.status(201)
|
|
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
|
-
|
|
158
|
+
})
|
|
159
|
+
.catch((error) => {
|
|
155
160
|
console.error('Error creating document:', error);
|
|
156
|
-
return res.status(500).json({
|
|
157
|
-
|
|
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
|
/**
|
package/libs/cloud/index.js
CHANGED
|
@@ -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 =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
7
|
-
title: {
|
|
8
|
-
type: String,
|
|
6
|
+
title: {type: String, trim: true, index: true,
|
|
9
7
|
required: [true, "Blog title is required"],
|
|
10
|
-
|
|
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: [
|
|
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: [
|
|
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: [
|
|
280
|
+
maxlength: [170, "Meta title cannot exceed 170 characters"]
|
|
289
281
|
},
|
|
290
282
|
metaDescription: {
|
|
291
283
|
type: String,
|
|
292
|
-
maxlength: [
|
|
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