ante-erp-cli 1.11.60 → 1.11.62
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/package.json +1 -1
- package/src/commands/set-domain.js +95 -43
- package/src/commands/ssl-enable.js +76 -23
- package/src/utils/nginx.js +142 -229
- package/src/utils/ssl.js +19 -5
package/package.json
CHANGED
|
@@ -914,64 +914,116 @@ export async function setDomain(options) {
|
|
|
914
914
|
}
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
// Track successful and failed domains
|
|
918
|
+
const successfulDomains = [];
|
|
919
|
+
const failedDomains = [];
|
|
920
|
+
|
|
921
|
+
// Frontend and API are mandatory - they must succeed
|
|
922
|
+
const mandatoryDomains = [frontendDomain];
|
|
923
|
+
if (apiDomain !== frontendDomain) {
|
|
924
|
+
mandatoryDomains.push(apiDomain);
|
|
925
|
+
}
|
|
926
|
+
|
|
917
927
|
for (const domain of domains) {
|
|
918
928
|
console.log(chalk.gray(` Obtaining certificate for ${domain}...`));
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
929
|
+
try {
|
|
930
|
+
await obtainSSLCertificate({
|
|
931
|
+
domain,
|
|
932
|
+
email: sslEmail,
|
|
933
|
+
staging: options.sslStaging || false
|
|
934
|
+
});
|
|
935
|
+
successfulDomains.push(domain);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
const isMandatory = mandatoryDomains.includes(domain);
|
|
938
|
+
if (isMandatory) {
|
|
939
|
+
// Mandatory domain failed - add to failures but continue
|
|
940
|
+
console.log(chalk.red(`✗ Failed (mandatory): ${domain}`));
|
|
941
|
+
failedDomains.push({ domain, error: error.message, mandatory: true });
|
|
942
|
+
} else {
|
|
943
|
+
// Optional domain failed - continue with others
|
|
944
|
+
console.log(chalk.yellow(`⚠ Skipped (optional): ${domain} - ${error.message}`));
|
|
945
|
+
failedDomains.push({ domain, error: error.message, mandatory: false });
|
|
946
|
+
}
|
|
947
|
+
}
|
|
924
948
|
}
|
|
925
949
|
|
|
926
|
-
|
|
950
|
+
// Show SSL certificate summary
|
|
951
|
+
console.log(chalk.cyan('\n📋 SSL Certificate Results:\n'));
|
|
952
|
+
successfulDomains.forEach(d => console.log(chalk.green(` ✓ ${d} - Certificate obtained`)));
|
|
953
|
+
failedDomains.forEach(f => console.log(chalk.yellow(` ⚠ ${f.domain} - ${f.error}`)));
|
|
927
954
|
|
|
928
|
-
//
|
|
929
|
-
|
|
955
|
+
// Check if any mandatory domains failed
|
|
956
|
+
const mandatoryFailures = failedDomains.filter(f => f.mandatory);
|
|
957
|
+
if (mandatoryFailures.length > 0) {
|
|
958
|
+
console.log(chalk.red('\n✗ Mandatory domains failed to get SSL certificates.'));
|
|
959
|
+
console.log(chalk.yellow(' NGINX will be configured with HTTP for failed domains.\n'));
|
|
960
|
+
}
|
|
930
961
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
frontendPort: 8080,
|
|
935
|
-
apiPort: 3001
|
|
936
|
-
};
|
|
962
|
+
// Proceed only if at least some domains succeeded
|
|
963
|
+
if (successfulDomains.length > 0) {
|
|
964
|
+
console.log(chalk.green(`\n✓ ${successfulDomains.length} of ${domains.length} SSL certificates obtained\n`));
|
|
937
965
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
sslNginxConfig.gateAppPort = 8081;
|
|
941
|
-
}
|
|
966
|
+
// Step 2: Update NGINX for HTTPS (with partial SSL support)
|
|
967
|
+
console.log(chalk.cyan('🔧 Configuring NGINX for HTTPS...\n'));
|
|
942
968
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
969
|
+
const sslNginxConfig = {
|
|
970
|
+
frontendDomain: frontendUrl,
|
|
971
|
+
apiDomain: apiUrl,
|
|
972
|
+
frontendPort: 8080,
|
|
973
|
+
apiPort: 3001,
|
|
974
|
+
domainsWithCerts: successfulDomains
|
|
975
|
+
};
|
|
947
976
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
977
|
+
if (hasGateApp && gateAppUrl) {
|
|
978
|
+
sslNginxConfig.gateAppDomain = gateAppUrl;
|
|
979
|
+
sslNginxConfig.gateAppPort = 8081;
|
|
980
|
+
}
|
|
952
981
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
982
|
+
if (hasGuardianApp && guardianAppUrl) {
|
|
983
|
+
sslNginxConfig.guardianAppDomain = guardianAppUrl;
|
|
984
|
+
sslNginxConfig.guardianAppPort = 8082;
|
|
985
|
+
}
|
|
957
986
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
987
|
+
if (hasFacialWeb && facialWebUrl) {
|
|
988
|
+
sslNginxConfig.facialAppDomain = facialWebUrl;
|
|
989
|
+
sslNginxConfig.facialWebPort = 8083;
|
|
990
|
+
}
|
|
962
991
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
992
|
+
if (hasPosApp && posAppUrl) {
|
|
993
|
+
sslNginxConfig.posAppDomain = posAppUrl;
|
|
994
|
+
sslNginxConfig.posAppPort = 8084;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (hasMlmApp && mlmAppUrl) {
|
|
998
|
+
sslNginxConfig.mlmAppDomain = mlmAppUrl;
|
|
999
|
+
sslNginxConfig.mlmAppPort = 9005;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (hasBackendMlm && backendMlmUrl) {
|
|
1003
|
+
sslNginxConfig.backendMlmDomain = backendMlmUrl;
|
|
1004
|
+
sslNginxConfig.backendMlmPort = 4001;
|
|
1005
|
+
}
|
|
967
1006
|
|
|
968
|
-
|
|
1007
|
+
await updateNginxForSSL(sslNginxConfig);
|
|
969
1008
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1009
|
+
// Step 3: Setup auto-renewal
|
|
1010
|
+
console.log(chalk.cyan('🔄 Setting up automatic certificate renewal...\n'));
|
|
1011
|
+
await setupAutoRenewal();
|
|
973
1012
|
|
|
974
|
-
|
|
1013
|
+
if (failedDomains.length > 0) {
|
|
1014
|
+
console.log(chalk.yellow('⚠ SSL/HTTPS partially configured\n'));
|
|
1015
|
+
console.log(chalk.gray(' Failed domains will use HTTP only.'));
|
|
1016
|
+
console.log(chalk.gray(' You can retry failed domains later with:'));
|
|
1017
|
+
console.log(chalk.gray(` ante ssl enable --email ${sslEmail}\n`));
|
|
1018
|
+
} else {
|
|
1019
|
+
console.log(chalk.green('✓ SSL/HTTPS configured successfully\n'));
|
|
1020
|
+
}
|
|
1021
|
+
} else {
|
|
1022
|
+
console.log(chalk.yellow('\n⚠ No SSL certificates were obtained.'));
|
|
1023
|
+
console.log(chalk.gray(' NGINX will use HTTP only.'));
|
|
1024
|
+
console.log(chalk.gray(' You can enable SSL later with:'));
|
|
1025
|
+
console.log(chalk.gray(` ante ssl enable --email ${sslEmail}\n`));
|
|
1026
|
+
}
|
|
975
1027
|
|
|
976
1028
|
} catch (error) {
|
|
977
1029
|
console.log(chalk.red('\n✗ SSL setup failed:'), error.message);
|
|
@@ -195,18 +195,23 @@ export async function sslEnable(options) {
|
|
|
195
195
|
|
|
196
196
|
console.log();
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
// In non-interactive mode, automatically proceed with renewal/reconfiguration
|
|
199
|
+
if (options.interactive !== false) {
|
|
200
|
+
const { proceed } = await inquirer.prompt([
|
|
201
|
+
{
|
|
202
|
+
type: 'confirm',
|
|
203
|
+
name: 'proceed',
|
|
204
|
+
message: 'Do you want to renew or reconfigure SSL certificates?',
|
|
205
|
+
default: false
|
|
206
|
+
}
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
if (!proceed) {
|
|
210
|
+
console.log(chalk.gray('\nSSL configuration cancelled.\n'));
|
|
211
|
+
return;
|
|
204
212
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (!proceed) {
|
|
208
|
-
console.log(chalk.gray('\nSSL configuration cancelled.\n'));
|
|
209
|
-
return;
|
|
213
|
+
} else {
|
|
214
|
+
console.log(chalk.gray('Non-interactive mode: Proceeding with SSL reconfiguration...\n'));
|
|
210
215
|
}
|
|
211
216
|
}
|
|
212
217
|
|
|
@@ -368,9 +373,21 @@ export async function sslEnable(options) {
|
|
|
368
373
|
|
|
369
374
|
console.log(chalk.bold('\n🚀 Starting SSL Configuration...\n'));
|
|
370
375
|
|
|
371
|
-
// Step 1: Obtain SSL certificates for all domains
|
|
376
|
+
// Step 1: Obtain SSL certificates for all domains (with graceful error handling)
|
|
372
377
|
console.log(chalk.cyan('📋 Obtaining SSL certificates...\n'));
|
|
373
378
|
|
|
379
|
+
// Track successful and failed domains
|
|
380
|
+
const successfulDomains = [];
|
|
381
|
+
const failedDomains = [];
|
|
382
|
+
|
|
383
|
+
// Extract domain names for the frontend and API (mandatory)
|
|
384
|
+
const frontendDomainName = frontendUrl.replace(/^https?:\/\//, '').replace(/:\d+$/, '');
|
|
385
|
+
const apiDomainName = apiUrl.replace(/^https?:\/\//, '').replace(/:\d+$/, '');
|
|
386
|
+
const mandatoryDomains = [frontendDomainName];
|
|
387
|
+
if (apiDomainName !== frontendDomainName) {
|
|
388
|
+
mandatoryDomains.push(apiDomainName);
|
|
389
|
+
}
|
|
390
|
+
|
|
374
391
|
for (const domainConfig of domains) {
|
|
375
392
|
try {
|
|
376
393
|
console.log(chalk.gray(` Obtaining certificate for ${domainConfig.domain}...`));
|
|
@@ -379,18 +396,36 @@ export async function sslEnable(options) {
|
|
|
379
396
|
email: options.email,
|
|
380
397
|
staging: options.staging || false
|
|
381
398
|
});
|
|
399
|
+
successfulDomains.push(domainConfig.domain);
|
|
382
400
|
} catch (error) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
401
|
+
const isMandatory = mandatoryDomains.includes(domainConfig.domain);
|
|
402
|
+
if (isMandatory) {
|
|
403
|
+
console.log(chalk.red(`✗ Failed (mandatory): ${domainConfig.domain}`));
|
|
404
|
+
failedDomains.push({ domain: domainConfig.domain, error: error.message, mandatory: true });
|
|
405
|
+
} else {
|
|
406
|
+
console.log(chalk.yellow(`⚠ Skipped (optional): ${domainConfig.domain} - ${error.message}`));
|
|
407
|
+
failedDomains.push({ domain: domainConfig.domain, error: error.message, mandatory: false });
|
|
408
|
+
}
|
|
390
409
|
}
|
|
391
410
|
}
|
|
392
411
|
|
|
393
|
-
|
|
412
|
+
// Show SSL certificate summary
|
|
413
|
+
console.log(chalk.cyan('\n📋 SSL Certificate Results:\n'));
|
|
414
|
+
successfulDomains.forEach(d => console.log(chalk.green(` ✓ ${d} - Certificate obtained`)));
|
|
415
|
+
failedDomains.forEach(f => console.log(chalk.yellow(` ⚠ ${f.domain} - ${f.error}`)));
|
|
416
|
+
|
|
417
|
+
// Check if we can proceed
|
|
418
|
+
if (successfulDomains.length === 0) {
|
|
419
|
+
console.log(chalk.red('\n✗ No SSL certificates were obtained.'));
|
|
420
|
+
console.log(chalk.yellow('\n⚠ Troubleshooting:'));
|
|
421
|
+
console.log(chalk.gray(' 1. Verify DNS points to this server'));
|
|
422
|
+
console.log(chalk.gray(' 2. Ensure ports 80 and 443 are open'));
|
|
423
|
+
console.log(chalk.gray(' 3. Check NGINX is running: systemctl status nginx'));
|
|
424
|
+
console.log(chalk.gray(' 4. View certbot logs: /var/log/letsencrypt/letsencrypt.log\n'));
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log(chalk.green(`\n✓ ${successfulDomains.length} of ${domains.length} SSL certificates obtained\n`));
|
|
394
429
|
|
|
395
430
|
// Step 2: Update NGINX configuration with SSL
|
|
396
431
|
console.log(chalk.cyan('🔧 Configuring NGINX for HTTPS...\n'));
|
|
@@ -404,7 +439,8 @@ export async function sslEnable(options) {
|
|
|
404
439
|
frontendDomain: frontendUrl,
|
|
405
440
|
apiDomain: apiUrl,
|
|
406
441
|
frontendPort,
|
|
407
|
-
apiPort
|
|
442
|
+
apiPort,
|
|
443
|
+
domainsWithCerts: successfulDomains
|
|
408
444
|
};
|
|
409
445
|
|
|
410
446
|
if (gateAppUrl) {
|
|
@@ -452,15 +488,32 @@ export async function sslEnable(options) {
|
|
|
452
488
|
await setupAutoRenewal();
|
|
453
489
|
|
|
454
490
|
// Success summary
|
|
455
|
-
|
|
491
|
+
if (failedDomains.length > 0) {
|
|
492
|
+
console.log(chalk.bold.yellow('\n⚠ SSL/HTTPS Configuration Partially Complete!\n'));
|
|
493
|
+
console.log(chalk.yellow('Some domains failed to get SSL certificates:'));
|
|
494
|
+
failedDomains.forEach(f => {
|
|
495
|
+
console.log(chalk.gray(` • ${f.domain}: ${f.error}`));
|
|
496
|
+
});
|
|
497
|
+
console.log(chalk.gray('\nFailed domains will use HTTP only until DNS is configured.'));
|
|
498
|
+
console.log(chalk.gray('You can retry later with: ante ssl enable --email ' + options.email + '\n'));
|
|
499
|
+
} else {
|
|
500
|
+
console.log(chalk.bold.green('\n✓ SSL/HTTPS Configuration Complete!\n'));
|
|
501
|
+
}
|
|
456
502
|
|
|
457
503
|
console.log(chalk.cyan('Secured Domains:'));
|
|
458
|
-
domains.forEach(d => {
|
|
504
|
+
domains.filter(d => successfulDomains.includes(d.domain)).forEach(d => {
|
|
459
505
|
const httpsUrl = d.url.replace('http://', 'https://').replace(/:\d+$/, '');
|
|
460
506
|
console.log(chalk.white(` • ${d.domain}`));
|
|
461
507
|
console.log(chalk.gray(` ${httpsUrl}\n`));
|
|
462
508
|
});
|
|
463
509
|
|
|
510
|
+
if (failedDomains.length > 0) {
|
|
511
|
+
console.log(chalk.yellow('Unsecured Domains (HTTP only):'));
|
|
512
|
+
failedDomains.forEach(f => {
|
|
513
|
+
console.log(chalk.gray(` • ${f.domain} - DNS not configured\n`));
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
464
517
|
console.log(chalk.cyan('Certificate Details:'));
|
|
465
518
|
console.log(chalk.gray(' Provider: Let\'s Encrypt'));
|
|
466
519
|
console.log(chalk.gray(' Validity: 90 days'));
|
package/src/utils/nginx.js
CHANGED
|
@@ -85,7 +85,8 @@ export function generateNginxConfig(config) {
|
|
|
85
85
|
mlmAppPort = 9005,
|
|
86
86
|
backendMlmPort = 4001,
|
|
87
87
|
ssl = false,
|
|
88
|
-
cloudflareSsl = false
|
|
88
|
+
cloudflareSsl = false,
|
|
89
|
+
domainsWithCerts = []
|
|
89
90
|
} = config;
|
|
90
91
|
|
|
91
92
|
// Extract domain without protocol
|
|
@@ -138,7 +139,8 @@ export function generateNginxConfig(config) {
|
|
|
138
139
|
facialWebPort,
|
|
139
140
|
posAppPort,
|
|
140
141
|
mlmAppPort,
|
|
141
|
-
backendMlmPort
|
|
142
|
+
backendMlmPort,
|
|
143
|
+
domainsWithCerts
|
|
142
144
|
});
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -782,6 +784,7 @@ export async function generateCloudflareCert() {
|
|
|
782
784
|
* @param {number} [config.posAppPort] - POS app port (default: 8084)
|
|
783
785
|
* @param {number} [config.mlmAppPort] - MLM app port (default: 9005)
|
|
784
786
|
* @param {number} [config.backendMlmPort] - Backend MLM API port (default: 4001)
|
|
787
|
+
* @param {string[]} [config.domainsWithCerts] - List of domains that have valid SSL certificates
|
|
785
788
|
* @returns {string} NGINX configuration with SSL
|
|
786
789
|
*/
|
|
787
790
|
function generateSslNginxConfig(config) {
|
|
@@ -801,19 +804,26 @@ function generateSslNginxConfig(config) {
|
|
|
801
804
|
facialWebPort = 8083,
|
|
802
805
|
posAppPort = 8084,
|
|
803
806
|
mlmAppPort = 9005,
|
|
804
|
-
backendMlmPort = 4001
|
|
807
|
+
backendMlmPort = 4001,
|
|
808
|
+
domainsWithCerts = []
|
|
805
809
|
} = config;
|
|
806
810
|
|
|
807
|
-
|
|
811
|
+
// Helper function to check if a domain has a valid SSL certificate
|
|
812
|
+
// If domainsWithCerts is empty, assume all domains have certs (backward compatibility)
|
|
813
|
+
const hasCert = (host) => domainsWithCerts.length === 0 || domainsWithCerts.includes(host);
|
|
814
|
+
|
|
815
|
+
// Helper function to generate SSL server block
|
|
816
|
+
const generateSslServerBlock = (host, port, appName, extraConfig = '') => `
|
|
817
|
+
# ${appName} Configuration (HTTPS)
|
|
808
818
|
server {
|
|
809
819
|
listen 443 ssl;
|
|
810
820
|
listen [::]:443 ssl;
|
|
811
821
|
http2 on;
|
|
812
|
-
server_name ${
|
|
822
|
+
server_name ${host};
|
|
813
823
|
|
|
814
824
|
# SSL Certificate paths
|
|
815
|
-
ssl_certificate /etc/letsencrypt/live/${
|
|
816
|
-
ssl_certificate_key /etc/letsencrypt/live/${
|
|
825
|
+
ssl_certificate /etc/letsencrypt/live/${host}/fullchain.pem;
|
|
826
|
+
ssl_certificate_key /etc/letsencrypt/live/${host}/privkey.pem;
|
|
817
827
|
|
|
818
828
|
# SSL Configuration
|
|
819
829
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
@@ -834,9 +844,9 @@ server {
|
|
|
834
844
|
|
|
835
845
|
# Increase body size for file uploads
|
|
836
846
|
client_max_body_size 100M;
|
|
837
|
-
|
|
847
|
+
${extraConfig}
|
|
838
848
|
location / {
|
|
839
|
-
proxy_pass http://localhost:${
|
|
849
|
+
proxy_pass http://localhost:${port};
|
|
840
850
|
proxy_http_version 1.1;
|
|
841
851
|
proxy_set_header Upgrade $http_upgrade;
|
|
842
852
|
proxy_set_header Connection 'upgrade';
|
|
@@ -853,37 +863,21 @@ server {
|
|
|
853
863
|
}
|
|
854
864
|
}
|
|
855
865
|
|
|
856
|
-
# HTTP to HTTPS redirect for ${
|
|
866
|
+
# HTTP to HTTPS redirect for ${host}
|
|
857
867
|
server {
|
|
858
868
|
listen 80;
|
|
859
869
|
listen [::]:80;
|
|
860
|
-
server_name ${
|
|
870
|
+
server_name ${host};
|
|
861
871
|
return 301 https://$server_name$request_uri;
|
|
862
|
-
}
|
|
872
|
+
}`;
|
|
863
873
|
|
|
864
|
-
|
|
874
|
+
// Helper function to generate HTTP-only server block (for domains without SSL cert)
|
|
875
|
+
const generateHttpServerBlock = (host, port, appName, extraConfig = '') => `
|
|
876
|
+
# ${appName} Configuration (HTTP only - SSL cert not available)
|
|
865
877
|
server {
|
|
866
|
-
listen
|
|
867
|
-
listen [::]:
|
|
868
|
-
|
|
869
|
-
server_name ${apiHost};
|
|
870
|
-
|
|
871
|
-
# SSL Certificate paths
|
|
872
|
-
ssl_certificate /etc/letsencrypt/live/${apiHost}/fullchain.pem;
|
|
873
|
-
ssl_certificate_key /etc/letsencrypt/live/${apiHost}/privkey.pem;
|
|
874
|
-
|
|
875
|
-
# SSL Configuration
|
|
876
|
-
ssl_protocols TLSv1.2 TLSv1.3;
|
|
877
|
-
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
878
|
-
ssl_prefer_server_ciphers on;
|
|
879
|
-
ssl_session_cache shared:SSL:10m;
|
|
880
|
-
ssl_session_timeout 10m;
|
|
881
|
-
|
|
882
|
-
# Security headers
|
|
883
|
-
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
884
|
-
add_header X-Frame-Options SAMEORIGIN always;
|
|
885
|
-
add_header X-Content-Type-Options nosniff always;
|
|
886
|
-
add_header X-XSS-Protection "1; mode=block" always;
|
|
878
|
+
listen 80;
|
|
879
|
+
listen [::]:80;
|
|
880
|
+
server_name ${host};
|
|
887
881
|
|
|
888
882
|
# Increase buffer sizes for large headers
|
|
889
883
|
client_header_buffer_size 16k;
|
|
@@ -891,9 +885,9 @@ server {
|
|
|
891
885
|
|
|
892
886
|
# Increase body size for file uploads
|
|
893
887
|
client_max_body_size 100M;
|
|
894
|
-
|
|
888
|
+
${extraConfig}
|
|
895
889
|
location / {
|
|
896
|
-
proxy_pass http://localhost:${
|
|
890
|
+
proxy_pass http://localhost:${port};
|
|
897
891
|
proxy_http_version 1.1;
|
|
898
892
|
proxy_set_header Upgrade $http_upgrade;
|
|
899
893
|
proxy_set_header Connection 'upgrade';
|
|
@@ -907,31 +901,40 @@ server {
|
|
|
907
901
|
proxy_connect_timeout 60s;
|
|
908
902
|
proxy_send_timeout 60s;
|
|
909
903
|
proxy_read_timeout 60s;
|
|
904
|
+
}
|
|
905
|
+
}`;
|
|
910
906
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
907
|
+
// Helper function to generate appropriate block based on cert availability
|
|
908
|
+
const generateServerBlock = (host, port, appName, extraConfig = '') => {
|
|
909
|
+
if (hasCert(host)) {
|
|
910
|
+
return generateSslServerBlock(host, port, appName, extraConfig);
|
|
914
911
|
}
|
|
915
|
-
|
|
912
|
+
return generateHttpServerBlock(host, port, appName, extraConfig);
|
|
913
|
+
};
|
|
916
914
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
#
|
|
915
|
+
// Generate config blocks
|
|
916
|
+
let config_output = '';
|
|
917
|
+
|
|
918
|
+
// Frontend (required)
|
|
919
|
+
config_output += generateServerBlock(frontendHost, frontendPort, 'ANTE Frontend');
|
|
920
|
+
|
|
921
|
+
// API (required) - with WebSocket support
|
|
922
|
+
const apiExtraConfig = `
|
|
923
|
+
# WebSocket support
|
|
924
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
925
|
+
proxy_set_header X-Forwarded-Server $host;`;
|
|
926
|
+
if (hasCert(apiHost)) {
|
|
927
|
+
config_output += `
|
|
928
|
+
# ANTE API Configuration (HTTPS)
|
|
926
929
|
server {
|
|
927
930
|
listen 443 ssl;
|
|
928
931
|
listen [::]:443 ssl;
|
|
929
932
|
http2 on;
|
|
930
|
-
server_name ${
|
|
933
|
+
server_name ${apiHost};
|
|
931
934
|
|
|
932
935
|
# SSL Certificate paths
|
|
933
|
-
ssl_certificate /etc/letsencrypt/live/${
|
|
934
|
-
ssl_certificate_key /etc/letsencrypt/live/${
|
|
936
|
+
ssl_certificate /etc/letsencrypt/live/${apiHost}/fullchain.pem;
|
|
937
|
+
ssl_certificate_key /etc/letsencrypt/live/${apiHost}/privkey.pem;
|
|
935
938
|
|
|
936
939
|
# SSL Configuration
|
|
937
940
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
@@ -954,7 +957,7 @@ server {
|
|
|
954
957
|
client_max_body_size 100M;
|
|
955
958
|
|
|
956
959
|
location / {
|
|
957
|
-
proxy_pass http://localhost:${
|
|
960
|
+
proxy_pass http://localhost:${apiPort};
|
|
958
961
|
proxy_http_version 1.1;
|
|
959
962
|
proxy_set_header Upgrade $http_upgrade;
|
|
960
963
|
proxy_set_header Connection 'upgrade';
|
|
@@ -968,40 +971,27 @@ server {
|
|
|
968
971
|
proxy_connect_timeout 60s;
|
|
969
972
|
proxy_send_timeout 60s;
|
|
970
973
|
proxy_read_timeout 60s;
|
|
974
|
+
|
|
975
|
+
# WebSocket support
|
|
976
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
977
|
+
proxy_set_header X-Forwarded-Server $host;
|
|
971
978
|
}
|
|
972
979
|
}
|
|
973
980
|
|
|
974
|
-
# HTTP to HTTPS redirect for ${
|
|
981
|
+
# HTTP to HTTPS redirect for ${apiHost}
|
|
975
982
|
server {
|
|
976
983
|
listen 80;
|
|
977
984
|
listen [::]:80;
|
|
978
|
-
server_name ${
|
|
985
|
+
server_name ${apiHost};
|
|
979
986
|
return 301 https://$server_name$request_uri;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
|
|
987
|
+
}`;
|
|
988
|
+
} else {
|
|
989
|
+
config_output += `
|
|
990
|
+
# ANTE API Configuration (HTTP only - SSL cert not available)
|
|
983
991
|
server {
|
|
984
|
-
listen
|
|
985
|
-
listen [::]:
|
|
986
|
-
|
|
987
|
-
server_name ${guardianAppHost};
|
|
988
|
-
|
|
989
|
-
# SSL Certificate paths
|
|
990
|
-
ssl_certificate /etc/letsencrypt/live/${guardianAppHost}/fullchain.pem;
|
|
991
|
-
ssl_certificate_key /etc/letsencrypt/live/${guardianAppHost}/privkey.pem;
|
|
992
|
-
|
|
993
|
-
# SSL Configuration
|
|
994
|
-
ssl_protocols TLSv1.2 TLSv1.3;
|
|
995
|
-
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
996
|
-
ssl_prefer_server_ciphers on;
|
|
997
|
-
ssl_session_cache shared:SSL:10m;
|
|
998
|
-
ssl_session_timeout 10m;
|
|
999
|
-
|
|
1000
|
-
# Security headers
|
|
1001
|
-
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
1002
|
-
add_header X-Frame-Options SAMEORIGIN always;
|
|
1003
|
-
add_header X-Content-Type-Options nosniff always;
|
|
1004
|
-
add_header X-XSS-Protection "1; mode=block" always;
|
|
992
|
+
listen 80;
|
|
993
|
+
listen [::]:80;
|
|
994
|
+
server_name ${apiHost};
|
|
1005
995
|
|
|
1006
996
|
# Increase buffer sizes for large headers
|
|
1007
997
|
client_header_buffer_size 16k;
|
|
@@ -1011,7 +1001,7 @@ server {
|
|
|
1011
1001
|
client_max_body_size 100M;
|
|
1012
1002
|
|
|
1013
1003
|
location / {
|
|
1014
|
-
proxy_pass http://localhost:${
|
|
1004
|
+
proxy_pass http://localhost:${apiPort};
|
|
1015
1005
|
proxy_http_version 1.1;
|
|
1016
1006
|
proxy_set_header Upgrade $http_upgrade;
|
|
1017
1007
|
proxy_set_header Connection 'upgrade';
|
|
@@ -1025,17 +1015,38 @@ server {
|
|
|
1025
1015
|
proxy_connect_timeout 60s;
|
|
1026
1016
|
proxy_send_timeout 60s;
|
|
1027
1017
|
proxy_read_timeout 60s;
|
|
1018
|
+
|
|
1019
|
+
# WebSocket support
|
|
1020
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
1021
|
+
proxy_set_header X-Forwarded-Server $host;
|
|
1028
1022
|
}
|
|
1029
|
-
}
|
|
1023
|
+
}`;
|
|
1024
|
+
}
|
|
1030
1025
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1026
|
+
// Optional apps
|
|
1027
|
+
if (gateAppHost) {
|
|
1028
|
+
config_output += generateServerBlock(gateAppHost, gateAppPort, 'ANTE Gate App');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (guardianAppHost) {
|
|
1032
|
+
config_output += generateServerBlock(guardianAppHost, guardianAppPort, 'ANTE Guardian App');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (facialAppHost) {
|
|
1036
|
+
// Facial app has special config.js caching rules
|
|
1037
|
+
const facialExtraConfig = `
|
|
1038
|
+
# Disable caching for config.js to ensure runtime config is always fresh
|
|
1039
|
+
location = /config.js {
|
|
1040
|
+
proxy_pass http://localhost:${facialWebPort}/config.js;
|
|
1041
|
+
proxy_http_version 1.1;
|
|
1042
|
+
proxy_set_header Host $host;
|
|
1043
|
+
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
|
1044
|
+
add_header Pragma "no-cache" always;
|
|
1045
|
+
add_header Expires "0" always;
|
|
1046
|
+
}
|
|
1047
|
+
`;
|
|
1048
|
+
if (hasCert(facialAppHost)) {
|
|
1049
|
+
config_output += `
|
|
1039
1050
|
# ANTE Facial Web App Configuration (HTTPS)
|
|
1040
1051
|
server {
|
|
1041
1052
|
listen 443 ssl;
|
|
@@ -1101,98 +1112,34 @@ server {
|
|
|
1101
1112
|
listen [::]:80;
|
|
1102
1113
|
server_name ${facialAppHost};
|
|
1103
1114
|
return 301 https://$server_name$request_uri;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1115
|
+
}`;
|
|
1116
|
+
} else {
|
|
1117
|
+
config_output += `
|
|
1118
|
+
# ANTE Facial Web App Configuration (HTTP only - SSL cert not available)
|
|
1107
1119
|
server {
|
|
1108
|
-
listen
|
|
1109
|
-
listen [::]:
|
|
1110
|
-
|
|
1111
|
-
server_name ${posAppHost};
|
|
1112
|
-
|
|
1113
|
-
# SSL Certificate paths
|
|
1114
|
-
ssl_certificate /etc/letsencrypt/live/${posAppHost}/fullchain.pem;
|
|
1115
|
-
ssl_certificate_key /etc/letsencrypt/live/${posAppHost}/privkey.pem;
|
|
1116
|
-
|
|
1117
|
-
# SSL Configuration
|
|
1118
|
-
ssl_protocols TLSv1.2 TLSv1.3;
|
|
1119
|
-
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
1120
|
-
ssl_prefer_server_ciphers on;
|
|
1121
|
-
ssl_session_cache shared:SSL:10m;
|
|
1122
|
-
ssl_session_timeout 10m;
|
|
1123
|
-
|
|
1124
|
-
# Security headers
|
|
1125
|
-
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
1126
|
-
add_header X-Frame-Options SAMEORIGIN always;
|
|
1127
|
-
add_header X-Content-Type-Options nosniff always;
|
|
1128
|
-
add_header X-XSS-Protection "1; mode=block" always;
|
|
1120
|
+
listen 80;
|
|
1121
|
+
listen [::]:80;
|
|
1122
|
+
server_name ${facialAppHost};
|
|
1129
1123
|
|
|
1130
1124
|
# Increase buffer sizes for large headers
|
|
1131
1125
|
client_header_buffer_size 16k;
|
|
1132
1126
|
large_client_header_buffers 4 16k;
|
|
1133
1127
|
|
|
1134
|
-
# Increase body size for
|
|
1135
|
-
client_max_body_size
|
|
1128
|
+
# Increase body size for image uploads
|
|
1129
|
+
client_max_body_size 50M;
|
|
1136
1130
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1131
|
+
# Disable caching for config.js to ensure runtime config is always fresh
|
|
1132
|
+
location = /config.js {
|
|
1133
|
+
proxy_pass http://localhost:${facialWebPort}/config.js;
|
|
1139
1134
|
proxy_http_version 1.1;
|
|
1140
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
1141
|
-
proxy_set_header Connection 'upgrade';
|
|
1142
1135
|
proxy_set_header Host $host;
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
proxy_cache_bypass $http_upgrade;
|
|
1147
|
-
|
|
1148
|
-
# Timeout settings
|
|
1149
|
-
proxy_connect_timeout 60s;
|
|
1150
|
-
proxy_send_timeout 60s;
|
|
1151
|
-
proxy_read_timeout 60s;
|
|
1136
|
+
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
|
1137
|
+
add_header Pragma "no-cache" always;
|
|
1138
|
+
add_header Expires "0" always;
|
|
1152
1139
|
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
# HTTP to HTTPS redirect for ${posAppHost}
|
|
1156
|
-
server {
|
|
1157
|
-
listen 80;
|
|
1158
|
-
listen [::]:80;
|
|
1159
|
-
server_name ${posAppHost};
|
|
1160
|
-
return 301 https://$server_name$request_uri;
|
|
1161
|
-
}
|
|
1162
|
-
` : ''}${mlmAppHost ? `
|
|
1163
|
-
# ANTE MLM App Configuration (HTTPS)
|
|
1164
|
-
server {
|
|
1165
|
-
listen 443 ssl;
|
|
1166
|
-
listen [::]:443 ssl;
|
|
1167
|
-
http2 on;
|
|
1168
|
-
server_name ${mlmAppHost};
|
|
1169
|
-
|
|
1170
|
-
# SSL Certificate paths
|
|
1171
|
-
ssl_certificate /etc/letsencrypt/live/${mlmAppHost}/fullchain.pem;
|
|
1172
|
-
ssl_certificate_key /etc/letsencrypt/live/${mlmAppHost}/privkey.pem;
|
|
1173
|
-
|
|
1174
|
-
# SSL Configuration
|
|
1175
|
-
ssl_protocols TLSv1.2 TLSv1.3;
|
|
1176
|
-
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
1177
|
-
ssl_prefer_server_ciphers on;
|
|
1178
|
-
ssl_session_cache shared:SSL:10m;
|
|
1179
|
-
ssl_session_timeout 10m;
|
|
1180
|
-
|
|
1181
|
-
# Security headers
|
|
1182
|
-
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
1183
|
-
add_header X-Frame-Options SAMEORIGIN always;
|
|
1184
|
-
add_header X-Content-Type-Options nosniff always;
|
|
1185
|
-
add_header X-XSS-Protection "1; mode=block" always;
|
|
1186
|
-
|
|
1187
|
-
# Increase buffer sizes for large headers
|
|
1188
|
-
client_header_buffer_size 16k;
|
|
1189
|
-
large_client_header_buffers 4 16k;
|
|
1190
|
-
|
|
1191
|
-
# Increase body size for file uploads
|
|
1192
|
-
client_max_body_size 100M;
|
|
1193
1140
|
|
|
1194
1141
|
location / {
|
|
1195
|
-
proxy_pass http://localhost:${
|
|
1142
|
+
proxy_pass http://localhost:${facialWebPort};
|
|
1196
1143
|
proxy_http_version 1.1;
|
|
1197
1144
|
proxy_set_header Upgrade $http_upgrade;
|
|
1198
1145
|
proxy_set_header Connection 'upgrade';
|
|
@@ -1207,74 +1154,40 @@ server {
|
|
|
1207
1154
|
proxy_send_timeout 60s;
|
|
1208
1155
|
proxy_read_timeout 60s;
|
|
1209
1156
|
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
server {
|
|
1214
|
-
listen 80;
|
|
1215
|
-
listen [::]:80;
|
|
1216
|
-
server_name ${mlmAppHost};
|
|
1217
|
-
return 301 https://$server_name$request_uri;
|
|
1218
|
-
}
|
|
1219
|
-
` : ''}${backendMlmHost ? `
|
|
1220
|
-
# ANTE Backend MLM API Configuration (HTTPS)
|
|
1221
|
-
server {
|
|
1222
|
-
listen 443 ssl;
|
|
1223
|
-
listen [::]:443 ssl;
|
|
1224
|
-
http2 on;
|
|
1225
|
-
server_name ${backendMlmHost};
|
|
1157
|
+
}`;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1226
1160
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1161
|
+
if (posAppHost) {
|
|
1162
|
+
config_output += generateServerBlock(posAppHost, posAppPort, 'ANTE POS App');
|
|
1163
|
+
}
|
|
1230
1164
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
ssl_prefer_server_ciphers on;
|
|
1235
|
-
ssl_session_cache shared:SSL:10m;
|
|
1236
|
-
ssl_session_timeout 10m;
|
|
1165
|
+
if (mlmAppHost) {
|
|
1166
|
+
config_output += generateServerBlock(mlmAppHost, mlmAppPort, 'ANTE MLM App');
|
|
1167
|
+
}
|
|
1237
1168
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
add_header X-Content-Type-Options nosniff always;
|
|
1242
|
-
add_header X-XSS-Protection "1; mode=block" always;
|
|
1169
|
+
if (backendMlmHost) {
|
|
1170
|
+
config_output += generateServerBlock(backendMlmHost, backendMlmPort, 'ANTE Backend MLM API');
|
|
1171
|
+
}
|
|
1243
1172
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
large_client_header_buffers 4 16k;
|
|
1173
|
+
return config_output.trim();
|
|
1174
|
+
}
|
|
1247
1175
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1176
|
+
/**
|
|
1177
|
+
* Configure NGINX for ANTE domains - KEPT FOR REFERENCE BUT NOT USED
|
|
1178
|
+
* The generateSslNginxConfig function now handles both SSL and non-SSL domains
|
|
1179
|
+
* based on the domainsWithCerts parameter.
|
|
1180
|
+
*/
|
|
1250
1181
|
|
|
1251
|
-
|
|
1252
|
-
proxy_pass http://localhost:${backendMlmPort};
|
|
1253
|
-
proxy_http_version 1.1;
|
|
1254
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
1255
|
-
proxy_set_header Connection 'upgrade';
|
|
1256
|
-
proxy_set_header Host $host;
|
|
1257
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
1258
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1259
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
1260
|
-
proxy_cache_bypass $http_upgrade;
|
|
1182
|
+
// NOTE: Old generateSslNginxConfigLegacy function removed - functionality merged into generateSslNginxConfig
|
|
1261
1183
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Generate NGINX configuration for Cloudflare SSL - PLACEHOLDER FOR OLD FUNCTION LOCATION
|
|
1186
|
+
* This marks the end of the SSL config generation section
|
|
1187
|
+
*/
|
|
1268
1188
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
listen 80;
|
|
1272
|
-
listen [::]:80;
|
|
1273
|
-
server_name ${backendMlmHost};
|
|
1274
|
-
return 301 https://$server_name$request_uri;
|
|
1275
|
-
}
|
|
1276
|
-
` : ''}`;
|
|
1277
|
-
}
|
|
1189
|
+
// Previous generateSslNginxConfigLegacy function has been removed.
|
|
1190
|
+
// The new generateSslNginxConfig function handles partial SSL scenarios.
|
|
1278
1191
|
|
|
1279
1192
|
/**
|
|
1280
1193
|
* Configure NGINX for ANTE domains
|
package/src/utils/ssl.js
CHANGED
|
@@ -60,10 +60,11 @@ export async function checkDomainDNS(domain) {
|
|
|
60
60
|
* @param {string} config.domain - Domain name (e.g., ante.example.com)
|
|
61
61
|
* @param {string} config.email - Email for certificate notifications
|
|
62
62
|
* @param {boolean} [config.staging=false] - Use staging environment for testing
|
|
63
|
-
* @
|
|
63
|
+
* @param {boolean} [config.graceful=false] - If true, returns result object instead of throwing
|
|
64
|
+
* @returns {Promise<boolean|Object>} Returns true on success, or result object if graceful mode
|
|
64
65
|
*/
|
|
65
66
|
export async function obtainSSLCertificate(config) {
|
|
66
|
-
const { domain, email, staging = false } = config;
|
|
67
|
+
const { domain, email, staging = false, graceful = false } = config;
|
|
67
68
|
const spinner = ora(`Obtaining SSL certificate for ${domain}...`).start();
|
|
68
69
|
|
|
69
70
|
try {
|
|
@@ -109,12 +110,22 @@ export async function obtainSSLCertificate(config) {
|
|
|
109
110
|
spinner.succeed(`SSL certificate obtained for ${domain}`);
|
|
110
111
|
} catch (error) {
|
|
111
112
|
spinner.fail(`Failed to obtain SSL certificate for ${domain}`);
|
|
112
|
-
|
|
113
|
+
const errorMessage = `Certbot failed: ${error.message}`;
|
|
114
|
+
if (graceful) {
|
|
115
|
+
return { success: false, domain, error: errorMessage };
|
|
116
|
+
}
|
|
117
|
+
throw new Error(errorMessage);
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
if (graceful) {
|
|
121
|
+
return { success: true, domain };
|
|
122
|
+
}
|
|
115
123
|
return true;
|
|
116
124
|
} catch (error) {
|
|
117
125
|
spinner.fail('Failed to obtain SSL certificate');
|
|
126
|
+
if (graceful) {
|
|
127
|
+
return { success: false, domain, error: error.message };
|
|
128
|
+
}
|
|
118
129
|
throw error;
|
|
119
130
|
}
|
|
120
131
|
}
|
|
@@ -138,6 +149,7 @@ export async function obtainSSLCertificate(config) {
|
|
|
138
149
|
* @param {number} [config.posAppPort=8084] - POS App port
|
|
139
150
|
* @param {number} [config.mlmAppPort=9005] - MLM App port
|
|
140
151
|
* @param {number} [config.backendMlmPort=4001] - Backend MLM API port
|
|
152
|
+
* @param {string[]} [config.domainsWithCerts=[]] - List of domains that have valid SSL certificates
|
|
141
153
|
* @returns {Promise<void>}
|
|
142
154
|
*/
|
|
143
155
|
export async function updateNginxForSSL(config) {
|
|
@@ -157,7 +169,8 @@ export async function updateNginxForSSL(config) {
|
|
|
157
169
|
facialWebPort = 8083,
|
|
158
170
|
posAppPort = 8084,
|
|
159
171
|
mlmAppPort = 9005,
|
|
160
|
-
backendMlmPort = 4001
|
|
172
|
+
backendMlmPort = 4001,
|
|
173
|
+
domainsWithCerts = []
|
|
161
174
|
} = config;
|
|
162
175
|
const spinner = ora('Updating NGINX configuration for HTTPS...').start();
|
|
163
176
|
|
|
@@ -194,7 +207,8 @@ export async function updateNginxForSSL(config) {
|
|
|
194
207
|
posAppPort,
|
|
195
208
|
mlmAppPort,
|
|
196
209
|
backendMlmPort,
|
|
197
|
-
ssl: true
|
|
210
|
+
ssl: true,
|
|
211
|
+
domainsWithCerts
|
|
198
212
|
});
|
|
199
213
|
|
|
200
214
|
// Write new configuration
|