@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 +456 -30
- package/controllers/review.js +161 -164
- package/libs/emailer/smtp.js +2 -1
- package/libs/greenlock/setup.js +105 -0
- package/models/review.js +5 -4
- package/package.json +1 -1
- package/routes/review.js +9 -5
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
|
|
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
|
|
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.
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
477
|
+
* Setup process signal handlers for graceful shutdown
|
|
280
478
|
*/
|
|
281
479
|
setupProcessHandlers() {
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
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
|
|
464
|
-
|
|
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.
|
|
832
|
+
if (this.httpsConfig) {
|
|
833
|
+
this.httpsServer = server;
|
|
834
|
+
} else {
|
|
835
|
+
this.server = server;
|
|
836
|
+
}
|
|
837
|
+
|
|
468
838
|
logger.info('═══════════════════════════════════════');
|
|
469
|
-
|
|
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
|
-
|
|
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
|
|
package/controllers/review.js
CHANGED
|
@@ -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.
|
|
11
|
+
exports.add = async (req, res) => {
|
|
11
12
|
const { username, email, rating, comment } = req.body;
|
|
12
|
-
const productId = req.params.
|
|
13
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
product
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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(
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
}
|
package/libs/emailer/smtp.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
router.
|
|
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
|
};
|