nodejs-quickstart-structure 2.2.0 → 2.3.0
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/CHANGELOG.md +15 -0
- package/README.md +14 -13
- package/bin/index.js +2 -1
- package/lib/generator.js +10 -1
- package/lib/modules/project-setup.js +41 -0
- package/lib/modules/terraform-setup.js +131 -0
- package/lib/prompts.js +22 -3
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +2 -0
- package/templates/clean-architecture/js/src/usecases/CreateUser.js.ejs +5 -2
- package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
- package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +4 -2
- package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
- package/templates/common/auth/js/controllers/authController.js.ejs +42 -12
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +95 -5
- package/templates/common/auth/js/services/jwtService.js.ejs +3 -3
- package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +0 -2
- package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +29 -7
- package/templates/common/auth/ts/controllers/authController.ts.ejs +34 -5
- package/templates/common/caching/clean/js/CreateUser.js.ejs +4 -2
- package/templates/common/caching/clean/ts/createUser.ts.ejs +4 -2
- package/templates/common/eslint.config.mjs.ejs +4 -1
- package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
- package/templates/common/package.json.ejs +2 -0
- package/templates/common/terraform/main.tf +52 -0
- package/templates/common/terraform/modules/cache/main.tf +41 -0
- package/templates/common/terraform/modules/cache/outputs.tf +7 -0
- package/templates/common/terraform/modules/cache/variables.tf +4 -0
- package/templates/common/terraform/modules/compute/main.tf +69 -0
- package/templates/common/terraform/modules/compute/outputs.tf +7 -0
- package/templates/common/terraform/modules/compute/variables.tf +20 -0
- package/templates/common/terraform/modules/database/main.tf +57 -0
- package/templates/common/terraform/modules/database/outputs.tf +16 -0
- package/templates/common/terraform/modules/database/variables.tf +27 -0
- package/templates/common/terraform/modules/security/main.tf +130 -0
- package/templates/common/terraform/modules/security/outputs.tf +15 -0
- package/templates/common/terraform/modules/security/variables.tf +12 -0
- package/templates/common/terraform/modules/vpc/main.tf +134 -0
- package/templates/common/terraform/modules/vpc/outputs.tf +19 -0
- package/templates/common/terraform/modules/vpc/variables.tf +45 -0
- package/templates/common/terraform/outputs.tf +29 -0
- package/templates/common/terraform/provider.tf +17 -0
- package/templates/common/terraform/variables.tf +33 -0
- package/templates/mvc/js/src/index.js.ejs +2 -0
- package/templates/mvc/js/src/utils/httpCodes.js +1 -0
- package/templates/mvc/ts/src/index.ts.ejs +2 -0
- package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
2
2
|
|
|
3
|
+
## [2.3.0] - 2026-05-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Terraform Infrastructure Integration (IaC)**: Integrated modular, production-ready AWS Terraform templates with Multi-AZ VPC network isolation, WAF, ALB, RDS, and ElastiCache.
|
|
7
|
+
- **Expanded Validation Matrix**: Scaled the mathematical validation matrix to **23,760+ unique project scenarios** (representing a 3x expansion), ensuring 100% template rendering accuracy across all permutations including the new Infrastructure options.
|
|
8
|
+
|
|
9
|
+
## [2.2.1] - 2026-05-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Secure OAuth CSRF Protection**: Implemented robust state-validation using cryptographically secure random tokens and `HttpOnly` cookies to mitigate CSRF attacks in social authentication flows.
|
|
13
|
+
- **Improved CLI Robustness**: Enhanced argument parsing to support comma-separated strings for variadic flags like `--social-auth` and `--auth`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Architectural Parity & Type Safety**: Standardized `HTTP_STATUS.FORBIDDEN` across all templates and resolved TypeScript type inference issues (`never` type) in generated Clean Architecture controllers.
|
|
17
|
+
|
|
3
18
|
## [2.2.0] - 2026-05-05
|
|
4
19
|
|
|
5
20
|
### Added
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A powerful ecosystem to scaffold production-ready Node.js microservices with bui
|
|
|
17
17
|
- [What's New](#whats-new-in-v21-the-authentication-release)
|
|
18
18
|
- [Key Features](#key-features)
|
|
19
19
|
- [Professional Standards](#professional-standards)
|
|
20
|
-
- [
|
|
20
|
+
- [23,760+ Project Combinations](#23760-project-combinations)
|
|
21
21
|
- [Configuration Options](#configuration-options)
|
|
22
22
|
- [Generated Project Structure](#generated-project-structure)
|
|
23
23
|
- [Documentation](#documentation)
|
|
@@ -27,8 +27,8 @@ A powerful ecosystem to scaffold production-ready Node.js microservices with bui
|
|
|
27
27
|
|
|
28
28
|
| **Path A: Next-Gen Web UI** (Recommended) | **Path B: Interactive CLI** |
|
|
29
29
|
| :--- | :--- |
|
|
30
|
-
| <a href="https://
|
|
31
|
-
| [Try Visual Configurator →](https://
|
|
30
|
+
| <a href="https://nodejs-quickstart-generator.netlify.app/#configurator"><img src="docs/public/v2-preview.png" width="100%" alt="UI Preview"></a> | <img src="docs/demo.gif" width="100%" alt="CLI Demo"> |
|
|
31
|
+
| [Try Visual Configurator →](https://nodejs-quickstart-generator.netlify.app/#configurator) | [See CLI Commands ↓](#path-b-interactive-cli) |
|
|
32
32
|
| **Visual Preview**: Real-time folder simulation. | **Fast & Direct**: Quickly scaffold in terminal. |
|
|
33
33
|
| **Zero-Prompt**: Paste a tailored command. | **AI-Ready**: Generates `.cursorrules`. |
|
|
34
34
|
|
|
@@ -50,12 +50,12 @@ nodejs-quickstart init
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
-
## What's New in v2.
|
|
53
|
+
## What's New in v2.3 (The Infrastructure & Terraform Release)
|
|
54
54
|
|
|
55
|
-
The v2.
|
|
55
|
+
The v2.3.0 release brings professional-grade Infrastructure as Code (IaC) to your generated projects:
|
|
56
56
|
|
|
57
|
-
- **
|
|
58
|
-
- **Massive Matrix Expansion**: Now supporting **
|
|
57
|
+
- **Terraform Infrastructure Integration (IaC)**: Support for modular, production-ready AWS Terraform scaffolding with Multi-AZ VPC network isolation, WAF, application load balancers, database layers, and ElastiCache.
|
|
58
|
+
- **Massive Matrix Expansion**: Now supporting **23,760+ unique project scenarios**, mathematically validated for template consistency.
|
|
59
59
|
|
|
60
60
|
---
|
|
61
61
|
|
|
@@ -85,14 +85,14 @@ We don't just generate boilerplate; we generate **production-ready** foundations
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## 23,760+ Project Combinations
|
|
89
89
|
|
|
90
90
|
The CLI supports a massive number of configurations to fit your exact needs:
|
|
91
91
|
|
|
92
|
-
- **
|
|
93
|
-
- **MVC Architecture**:
|
|
94
|
-
- **Clean Architecture**:
|
|
95
|
-
- **
|
|
92
|
+
- **2,160 Core Combinations**:
|
|
93
|
+
- **MVC Architecture**: 1,620 variants (Languages × View Engines × Databases × Communication Patterns × Caching × Auth × Infrastructure)
|
|
94
|
+
- **Clean Architecture**: 540 variants (Languages × Databases × Communication Patterns × Caching × Auth × Infrastructure)
|
|
95
|
+
- **23,760+ Total Scenarios**:
|
|
96
96
|
- Every combination can be generated across 5 CI/CD providers.
|
|
97
97
|
- Optional **Enterprise-Grade Security Hardening** doubles the scenarios.
|
|
98
98
|
- Every single scenario is verified to be compatible with our **80% Coverage Threshold** policy.
|
|
@@ -112,6 +112,7 @@ The CLI will guide you through:
|
|
|
112
112
|
8. **Auth**: `None` | `JWT` | `OAuth2 (Google/GitHub) + JWT`
|
|
113
113
|
9. **CI/CD**: `GitHub Actions` | `Jenkins` | `GitLab CI` | `CircleCI` | `Bitbucket Pipelines`
|
|
114
114
|
10. **Security**: (Optional) Snyk & SonarCloud Hardening
|
|
115
|
+
11. **Infrastructure**: (Optional IaC) `None` | `Standard` (Single EC2) | `Production` (WAF + ALB + Multi-AZ VPC)
|
|
115
116
|
|
|
116
117
|
---
|
|
117
118
|
|
|
@@ -139,7 +140,7 @@ Depending on your choices, the structure adapts. Here is a **TypeScript + Clean
|
|
|
139
140
|
|
|
140
141
|
## Documentation
|
|
141
142
|
|
|
142
|
-
For full guides, architecture deep-dives, and feature references, visit our **[Official Documentation Site](https://
|
|
143
|
+
For full guides, architecture deep-dives, and feature references, visit our **[Official Documentation Site](https://nodejs-quickstart-generator.netlify.app/guide/getting-started.html)**.
|
|
143
144
|
|
|
144
145
|
---
|
|
145
146
|
|
package/bin/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const program = new Command();
|
|
|
15
15
|
|
|
16
16
|
program
|
|
17
17
|
.name('nodejs-quickstart')
|
|
18
|
-
.description('🚀 CLI to scaffold production-ready Node.js microservices.\n\nGenerates projects with:\n- MVC or Clean Architecture\n- REST, GraphQL or Kafka\n- MySQL, PostgreSQL, or MongoDB\n- Auth (None, JWT)\n- Docker, Flyway & Mongoose support')
|
|
18
|
+
.description('🚀 CLI to scaffold production-ready Node.js microservices.\n\nGenerates projects with:\n- MVC or Clean Architecture\n- REST, GraphQL or Kafka\n- MySQL, PostgreSQL, or MongoDB\n- Auth (None, JWT)\n- Terraform (Standard, Production)\n- Docker, Flyway & Mongoose support')
|
|
19
19
|
.version(pkg.version, '-v, --version', 'Output the current version')
|
|
20
20
|
.addHelpText('after', `\n${chalk.yellow('Example:')}\n $ nodejs-quickstart init ${chalk.gray('# Start the interactive setup')}\n`);
|
|
21
21
|
|
|
@@ -35,6 +35,7 @@ program
|
|
|
35
35
|
.option('--caching <type>', 'Caching Layer (None/Redis/Memory Cache)')
|
|
36
36
|
.option('--auth <modes...>', 'Authentication Modes (None, JWT)')
|
|
37
37
|
.option('--social-auth <providers...>', 'Social Authentication Providers (None, Google, GitHub)')
|
|
38
|
+
.option('--terraform <tier>', 'Infrastructure Tier (None, Standard, Production)')
|
|
38
39
|
.option('--advanced-options', 'Enable Advanced Options')
|
|
39
40
|
.option('--no-advanced-options', 'Disable Advanced Options')
|
|
40
41
|
.action(async (options) => {
|
package/lib/generator.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import { setupProjectDirectory, copyBaseStructure, copyCommonFiles } from './modules/project-setup.js';
|
|
3
|
+
import { setupProjectDirectory, copyBaseStructure, copyCommonFiles, cleanGeneratedFiles } from './modules/project-setup.js';
|
|
4
4
|
import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample, renderPm2Config, renderAiNativeFiles } from './modules/config-files.js';
|
|
5
5
|
import { renderIndexFile, renderEnvConfig, renderErrorMiddleware, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews, processAllTests } from './modules/app-setup.js';
|
|
6
6
|
import { setupDatabase } from './modules/database-setup.js';
|
|
7
7
|
import { setupKafka, setupViews } from './modules/kafka-setup.js';
|
|
8
8
|
import { setupCaching } from './modules/caching-setup.js';
|
|
9
|
+
import { setupTerraform } from './modules/terraform-setup.js';
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -23,6 +24,7 @@ export const generateProject = async (config) => {
|
|
|
23
24
|
database: 'None',
|
|
24
25
|
includeSecurity: false,
|
|
25
26
|
auth: ['None'],
|
|
27
|
+
terraform: 'None',
|
|
26
28
|
...config
|
|
27
29
|
};
|
|
28
30
|
|
|
@@ -99,6 +101,12 @@ export const generateProject = async (config) => {
|
|
|
99
101
|
// 16. PM2 Configuration
|
|
100
102
|
await renderPm2Config(templatesDir, targetDir, config);
|
|
101
103
|
|
|
104
|
+
// 16a. Terraform Setup
|
|
105
|
+
await setupTerraform(templatesDir, targetDir, config);
|
|
106
|
+
|
|
107
|
+
// 16b. Auto-format generated files (clean trailing whitespaces, collapse EJS conditional blank lines)
|
|
108
|
+
await cleanGeneratedFiles(targetDir);
|
|
109
|
+
|
|
102
110
|
// 17. Process All Tests
|
|
103
111
|
await processAllTests(targetDir, config);
|
|
104
112
|
|
|
@@ -122,6 +130,7 @@ export const generateProject = async (config) => {
|
|
|
122
130
|
✅ Security: Helmet, CORS, Rate-Limiting added${config.includeSecurity ? '\n ✅ Enterprise Security: Snyk (SCA) & SonarCloud (SAST) integration' : ''}
|
|
123
131
|
✅ Testing: Jest setup for Unit/Integration tests
|
|
124
132
|
✅ Docker: Production-ready multi-stage build
|
|
133
|
+
${config.terraform && config.terraform !== 'None' ? `✅ Infrastructure (IaC): Terraform (${config.terraform}) ready` : ''}
|
|
125
134
|
${config.ciProvider !== 'None' ? `✅ CI/CD: ${config.ciProvider} Workflow ready` : '❌ CI/CD: Skipped (User preferred)'}
|
|
126
135
|
|
|
127
136
|
----------------------------------------------------
|
|
@@ -31,3 +31,44 @@ export const copyCommonFiles = async (templatesDir, targetDir, language) => {
|
|
|
31
31
|
await fs.copy(path.join(templatesDir, 'common', 'tsconfig.eslint.json'), path.join(targetDir, 'tsconfig.eslint.json'));
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
|
|
35
|
+
export const cleanGeneratedFiles = async (dir) => {
|
|
36
|
+
const items = await fs.readdir(dir);
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
const fullPath = path.join(dir, item);
|
|
39
|
+
const stat = await fs.stat(fullPath);
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
if (item !== 'node_modules' && item !== '.git') {
|
|
42
|
+
await cleanGeneratedFiles(fullPath);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
const ext = path.extname(item);
|
|
46
|
+
if (['.ts', '.js', '.json', '.yml', '.yaml', '.md', '.env', '.example', '.cursorrules'].includes(ext) || item === 'Dockerfile' || item === 'Jenkinsfile') {
|
|
47
|
+
let content = await fs.readFile(fullPath, 'utf-8');
|
|
48
|
+
const original = content;
|
|
49
|
+
|
|
50
|
+
content = content.replace(/\r\n/g, '\n');
|
|
51
|
+
|
|
52
|
+
// 1. Remove trailing spaces on lines
|
|
53
|
+
content = content.replace(/[ \t]+$/gm, '');
|
|
54
|
+
|
|
55
|
+
// 2. Collapse 3+ consecutive newlines to maximum 2 newlines (double spacing)
|
|
56
|
+
content = content.replace(/\n{3,}/g, '\n\n');
|
|
57
|
+
|
|
58
|
+
// 3. Remove blank lines immediately before closing braces/brackets, preserving indentation
|
|
59
|
+
content = content.replace(/\n\s*\n(\s*)([\}|\]])/g, '\n$1$2');
|
|
60
|
+
|
|
61
|
+
// 4. Remove blank lines immediately after opening braces/brackets, preserving indentation
|
|
62
|
+
content = content.replace(/({\s*)\n\s*\n(\s*)/g, '$1\n$2');
|
|
63
|
+
content = content.replace(/(\[\s*)\n\s*\n(\s*)/g, '$1\n$2');
|
|
64
|
+
|
|
65
|
+
// 5. Ensure exactly one trailing newline at the end of the file
|
|
66
|
+
content = content.trim() + '\n';
|
|
67
|
+
|
|
68
|
+
if (content !== original) {
|
|
69
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const setupTerraform = async (templatesDir, targetDir, config) => {
|
|
6
|
+
const { terraform, projectName } = config;
|
|
7
|
+
|
|
8
|
+
if (!terraform || terraform === 'None') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const terraformSourceDir = path.join(templatesDir, 'common/terraform');
|
|
13
|
+
const terraformTargetDir = path.join(targetDir, 'terraform');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Ensure the source directory exists
|
|
17
|
+
if (!(await fs.pathExists(terraformSourceDir))) {
|
|
18
|
+
console.warn(chalk.yellow(`\n Terraform templates not found at ${terraformSourceDir}. Skipping...`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Create target directory
|
|
23
|
+
await fs.ensureDir(terraformTargetDir);
|
|
24
|
+
|
|
25
|
+
// Copy everything first
|
|
26
|
+
await fs.copy(terraformSourceDir, terraformTargetDir);
|
|
27
|
+
|
|
28
|
+
// --- Customization ---
|
|
29
|
+
|
|
30
|
+
// 1. Customize variables.tf
|
|
31
|
+
const variablesPath = path.join(terraformTargetDir, 'variables.tf');
|
|
32
|
+
if (await fs.pathExists(variablesPath)) {
|
|
33
|
+
let content = await fs.readFile(variablesPath, 'utf8');
|
|
34
|
+
content = content.replace(/default\s*=\s*"nodejs-quickstart"/, `default = "${projectName}"`);
|
|
35
|
+
|
|
36
|
+
// Map database engine
|
|
37
|
+
if (config.database === 'PostgreSQL') {
|
|
38
|
+
content = content.replace(/variable "db_engine" {[\s\S]*?default\s*=\s*"mysql"/, 'variable "db_engine" {\n description = "Database engine (mysql or postgres)"\n default = "postgres"');
|
|
39
|
+
} else if (config.database === 'MySQL') {
|
|
40
|
+
content = content.replace(/default\s*=\s*"myappdb"/, `default = "${config.dbName || 'demo'}"`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Set is_production flag
|
|
44
|
+
if (terraform === 'Production') {
|
|
45
|
+
content = content.replace(/variable "is_production" \{[\s\S]*?default\s*=\s*false/, 'variable "is_production" {\n description = "Enable production-grade features (Multi-AZ, WAF, Scaling)"\n type = bool\n default = true');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await fs.writeFile(variablesPath, content);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Handle Database "None" case
|
|
52
|
+
if (config.database === 'None') {
|
|
53
|
+
// REMOVE database module call from main.tf
|
|
54
|
+
const mainTfPath = path.join(terraformTargetDir, 'main.tf');
|
|
55
|
+
if (await fs.pathExists(mainTfPath)) {
|
|
56
|
+
let content = await fs.readFile(mainTfPath, 'utf8');
|
|
57
|
+
// Remove the database module block entirely (from comment to closing brace)
|
|
58
|
+
content = content.replace(/# --- Data Layer \(RDS Isolated\) ---[\s\S]*?module "database" \{[\s\S]*?\}\n/g, '');
|
|
59
|
+
await fs.writeFile(mainTfPath, content);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// REMOVE database outputs from outputs.tf
|
|
63
|
+
const outputsTfPath = path.join(terraformTargetDir, 'outputs.tf');
|
|
64
|
+
if (await fs.pathExists(outputsTfPath)) {
|
|
65
|
+
let content = await fs.readFile(outputsTfPath, 'utf8');
|
|
66
|
+
// Remove individual database outputs
|
|
67
|
+
content = content.replace(/output "database_[\s\S]*?\}\n/g, '');
|
|
68
|
+
await fs.writeFile(outputsTfPath, content);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// DELETE the database module folder
|
|
72
|
+
const dbModuleDir = path.join(terraformTargetDir, 'modules/database');
|
|
73
|
+
if (await fs.pathExists(dbModuleDir)) {
|
|
74
|
+
await fs.remove(dbModuleDir);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Handle Caching case (Delete if NOT Redis)
|
|
79
|
+
if (config.caching !== 'Redis') {
|
|
80
|
+
// REMOVE cache module call from main.tf
|
|
81
|
+
const mainTfPath = path.join(terraformTargetDir, 'main.tf');
|
|
82
|
+
if (await fs.pathExists(mainTfPath)) {
|
|
83
|
+
let content = await fs.readFile(mainTfPath, 'utf8');
|
|
84
|
+
// Remove the cache module block entirely
|
|
85
|
+
content = content.replace(/# --- Cache Layer \(Redis ElastiCache\) ---[\s\S]*?module "cache" \{[\s\S]*?\}\n/g, '');
|
|
86
|
+
await fs.writeFile(mainTfPath, content);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// REMOVE redis output from outputs.tf
|
|
90
|
+
const outputsTfPath = path.join(terraformTargetDir, 'outputs.tf');
|
|
91
|
+
if (await fs.pathExists(outputsTfPath)) {
|
|
92
|
+
let content = await fs.readFile(outputsTfPath, 'utf8');
|
|
93
|
+
content = content.replace(/output "redis_endpoint" \{[\s\S]*?\}\n/g, '');
|
|
94
|
+
await fs.writeFile(outputsTfPath, content);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// DELETE the cache module folder
|
|
98
|
+
const cacheModuleDir = path.join(terraformTargetDir, 'modules/cache');
|
|
99
|
+
if (await fs.pathExists(cacheModuleDir)) {
|
|
100
|
+
await fs.remove(cacheModuleDir);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// If caching IS Redis, it's already uncommented in the template, so no action needed here.
|
|
104
|
+
|
|
105
|
+
// 4. Customize Compute module based on language
|
|
106
|
+
const computeMainPath = path.join(terraformTargetDir, 'modules/compute/main.tf');
|
|
107
|
+
if (await fs.pathExists(computeMainPath)) {
|
|
108
|
+
let content = await fs.readFile(computeMainPath, 'utf8');
|
|
109
|
+
const startHint = config.language === 'TypeScript'
|
|
110
|
+
? 'npm run build && node dist/index.js'
|
|
111
|
+
: 'node src/index.js';
|
|
112
|
+
|
|
113
|
+
content = content.replace(/# docker run -d -p 3000:3000 my-node-app:latest/,
|
|
114
|
+
`# For ${config.language}: ${startHint}\n # docker run -d -p 3000:3000 my-node-app:latest`);
|
|
115
|
+
await fs.writeFile(computeMainPath, content);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 5. Handle "Standard" tier (Optional: disable WAF for cost)
|
|
119
|
+
if (terraform === 'Standard') {
|
|
120
|
+
const mainTfPath = path.join(terraformTargetDir, 'main.tf');
|
|
121
|
+
if (await fs.pathExists(mainTfPath)) {
|
|
122
|
+
let content = await fs.readFile(mainTfPath, 'utf8');
|
|
123
|
+
// In Standard tier, we might want to tell the security module to skip WAF
|
|
124
|
+
// For now, we'll keep it simple, but we could add a variable 'enable_waf'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(chalk.red(`\n Error setting up Terraform: ${error.message}`));
|
|
130
|
+
}
|
|
131
|
+
};
|
package/lib/prompts.js
CHANGED
|
@@ -89,7 +89,7 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
89
89
|
{
|
|
90
90
|
type: 'select',
|
|
91
91
|
name: 'advancedOptions',
|
|
92
|
-
message: 'Enable Advanced Options (Authentication, etc.)?',
|
|
92
|
+
message: 'Enable Advanced Options (Authentication, Terraform, etc.)?',
|
|
93
93
|
choices: ['No', 'Yes'],
|
|
94
94
|
default: 'No',
|
|
95
95
|
when: options.advancedOptions === undefined
|
|
@@ -104,6 +104,21 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
104
104
|
const advanced = options.advancedOptions !== undefined ? options.advancedOptions : answers.advancedOptions;
|
|
105
105
|
return (advanced === 'Yes' || advanced === true) && !options.auth;
|
|
106
106
|
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: 'select',
|
|
110
|
+
name: 'terraform',
|
|
111
|
+
message: 'Select Infrastructure (IaC - Terraform):',
|
|
112
|
+
choices: [
|
|
113
|
+
{ name: 'None (No infrastructure files)', value: 'None' },
|
|
114
|
+
{ name: 'Standard (Single EC2 - Cost Efficient)', value: 'Standard' },
|
|
115
|
+
{ name: 'Production (WAF + ALB + Private Subnets - High Availability)', value: 'Production' }
|
|
116
|
+
],
|
|
117
|
+
default: 'None',
|
|
118
|
+
when: (answers) => {
|
|
119
|
+
const advanced = options.advancedOptions !== undefined ? options.advancedOptions : answers.advancedOptions;
|
|
120
|
+
return (advanced === 'Yes' || advanced === true) && !options.terraform;
|
|
121
|
+
}
|
|
107
122
|
}
|
|
108
123
|
];
|
|
109
124
|
|
|
@@ -139,7 +154,9 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
139
154
|
|
|
140
155
|
// Normalize auth to array if it's a string from the options
|
|
141
156
|
if (typeof result.auth === 'string') {
|
|
142
|
-
result.auth =
|
|
157
|
+
result.auth = result.auth.split(',').map(s => s.trim());
|
|
158
|
+
} else if (Array.isArray(result.auth)) {
|
|
159
|
+
result.auth = result.auth.flatMap(s => s.split(',').map(ss => ss.trim()));
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
// Map friendly CLI strings to actual values
|
|
@@ -162,7 +179,9 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
162
179
|
|
|
163
180
|
// Normalize socialAuth to array from options
|
|
164
181
|
if (typeof result.socialAuth === 'string') {
|
|
165
|
-
result.socialAuth =
|
|
182
|
+
result.socialAuth = result.socialAuth.split(',').map(s => s.trim());
|
|
183
|
+
} else if (Array.isArray(result.socialAuth)) {
|
|
184
|
+
result.socialAuth = result.socialAuth.flatMap(s => s.split(',').map(ss => ss.trim()));
|
|
166
185
|
}
|
|
167
186
|
|
|
168
187
|
// Default socialAuth if not provided
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const cors = require('cors');
|
|
3
|
+
const cookieParser = require('cookie-parser');
|
|
3
4
|
const logger = require('../log/logger');
|
|
4
5
|
const morgan = require('morgan');
|
|
5
6
|
const { errorMiddleware } = require('./middleware/errorMiddleware');
|
|
@@ -30,6 +31,7 @@ const startServer = async () => {
|
|
|
30
31
|
const app = express();
|
|
31
32
|
|
|
32
33
|
app.use(cors());
|
|
34
|
+
app.use(cookieParser());
|
|
33
35
|
app.use(express.json());
|
|
34
36
|
app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));
|
|
35
37
|
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
@@ -13,12 +13,15 @@ class CreateUser {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async execute(name, email, password) {
|
|
16
|
+
<%_ if (auth.includes('JWT')) { -%>
|
|
16
17
|
let finalPassword = password;
|
|
17
|
-
<%_ if (auth.includes('JWT')) { -%>
|
|
18
18
|
if (password) {
|
|
19
19
|
finalPassword = await bcrypt.hash(password, 10);
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
21
|
const user = new User(null, name, email, finalPassword);
|
|
22
|
+
<%_ } else { -%>
|
|
23
|
+
const user = new User(null, name, email, password);
|
|
24
|
+
<%_ } -%>
|
|
22
25
|
const savedUser = await this.userRepository.save(user);
|
|
23
26
|
<%_ if (caching !== 'None') { -%>
|
|
24
27
|
try {
|
|
@@ -4,6 +4,7 @@ import cors from 'cors';
|
|
|
4
4
|
import helmet from 'helmet';
|
|
5
5
|
import hpp from 'hpp';
|
|
6
6
|
import rateLimit from 'express-rate-limit';
|
|
7
|
+
import cookieParser from 'cookie-parser';
|
|
7
8
|
import logger from '@/infrastructure/log/logger';
|
|
8
9
|
import morgan from 'morgan';
|
|
9
10
|
import { errorMiddleware } from '@/utils/errorMiddleware';
|
|
@@ -50,6 +51,7 @@ app.use(cors({ origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE'] }));
|
|
|
50
51
|
const limiter = rateLimit({ windowMs: 10 * 60 * 1000, max: 100 });
|
|
51
52
|
app.use(limiter);
|
|
52
53
|
|
|
54
|
+
app.use(cookieParser());
|
|
53
55
|
app.use(express.json());
|
|
54
56
|
app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } }));
|
|
55
57
|
|
|
@@ -14,13 +14,15 @@ export default class CreateUser {
|
|
|
14
14
|
constructor(private userRepository: UserRepository) {}
|
|
15
15
|
|
|
16
16
|
async execute(name: string, email: string, password?: string) {
|
|
17
|
-
let finalPassword = password;
|
|
18
17
|
<% if (auth.includes('JWT')) { -%>
|
|
18
|
+
let finalPassword = password;
|
|
19
19
|
if (password) {
|
|
20
20
|
finalPassword = await bcrypt.hash(password, 10);
|
|
21
21
|
}
|
|
22
|
-
<% } -%>
|
|
23
22
|
const user = new User(null, name, email, finalPassword);
|
|
23
|
+
<% } else { -%>
|
|
24
|
+
const user = new User(null, name, email, password);
|
|
25
|
+
<% } -%>
|
|
24
26
|
const savedUser = await this.userRepository.save(user);
|
|
25
27
|
<% if (caching !== 'None') { -%>
|
|
26
28
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const bcrypt = require('bcryptjs');
|
|
2
|
+
const crypto = require('crypto');
|
|
2
3
|
<%_ if (architecture === 'MVC') { _%>
|
|
3
4
|
const User = require('../models/User');
|
|
4
5
|
const JwtService = require('../services/jwtService');
|
|
@@ -40,6 +41,16 @@ class AuthController {
|
|
|
40
41
|
this.githubCallback = this.githubCallback.bind(this);
|
|
41
42
|
<% } -%>
|
|
42
43
|
<% } -%>
|
|
44
|
+
this.setOAuthStateCookie = this.setOAuthStateCookie.bind(this);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setOAuthStateCookie(res, state) {
|
|
48
|
+
res.cookie('oauth_state', state, {
|
|
49
|
+
httpOnly: true,
|
|
50
|
+
secure: process.env.NODE_ENV === 'production',
|
|
51
|
+
sameSite: 'lax',
|
|
52
|
+
maxAge: 10 * 60 * 1000
|
|
53
|
+
});
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
async login(req, res, next) {
|
|
@@ -296,7 +307,7 @@ class AuthController {
|
|
|
296
307
|
<%_ } _%>
|
|
297
308
|
|
|
298
309
|
res.json({ token: accessToken, accessToken, refreshToken });
|
|
299
|
-
<%
|
|
310
|
+
<% } %>
|
|
300
311
|
} catch (error) {
|
|
301
312
|
logger.error('Social exchange error:', error);
|
|
302
313
|
next(error);
|
|
@@ -307,6 +318,9 @@ class AuthController {
|
|
|
307
318
|
<% if (socialAuth.includes('Google')) { -%>
|
|
308
319
|
async googleLogin(req, res) {
|
|
309
320
|
const rootUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
321
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
322
|
+
this.setOAuthStateCookie(res, state);
|
|
323
|
+
|
|
310
324
|
const options = {
|
|
311
325
|
redirect_uri: process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/api/auth/google/callback',
|
|
312
326
|
client_id: process.env.GOOGLE_CLIENT_ID,
|
|
@@ -317,15 +331,22 @@ class AuthController {
|
|
|
317
331
|
'https://www.googleapis.com/auth/userinfo.profile',
|
|
318
332
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
319
333
|
].join(' '),
|
|
320
|
-
state:
|
|
334
|
+
state: state
|
|
321
335
|
};
|
|
322
336
|
const qs = new URLSearchParams(options);
|
|
323
337
|
res.redirect(`${rootUrl}?${qs.toString()}`);
|
|
324
338
|
}
|
|
325
339
|
|
|
326
|
-
async googleCallback(req, res,
|
|
340
|
+
async googleCallback(req, res, _next) {
|
|
327
341
|
try {
|
|
328
|
-
const { code } = req.query;
|
|
342
|
+
const { code, state } = req.query;
|
|
343
|
+
const savedState = req.cookies?.oauth_state;
|
|
344
|
+
res.clearCookie('oauth_state');
|
|
345
|
+
|
|
346
|
+
if (!state || state !== savedState) {
|
|
347
|
+
return res.status(HTTP_STATUS.FORBIDDEN).json({ message: 'Invalid state parameter' });
|
|
348
|
+
}
|
|
349
|
+
|
|
329
350
|
const redirectUri = process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/api/auth/google/callback';
|
|
330
351
|
|
|
331
352
|
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
@@ -344,7 +365,7 @@ class AuthController {
|
|
|
344
365
|
const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
345
366
|
activeTokens.push(refreshJti);
|
|
346
367
|
JwtService.activeRefreshTokens.set(userId, activeTokens);
|
|
347
|
-
<%
|
|
368
|
+
<% } %>
|
|
348
369
|
|
|
349
370
|
res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
|
|
350
371
|
res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
|
|
@@ -382,12 +403,12 @@ class AuthController {
|
|
|
382
403
|
const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
383
404
|
activeTokens.push(refreshJti);
|
|
384
405
|
JwtService.activeRefreshTokens.set(userId, activeTokens);
|
|
385
|
-
<%
|
|
406
|
+
<% } %>
|
|
386
407
|
|
|
387
408
|
res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
|
|
388
409
|
res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
|
|
389
410
|
res.redirect('/');
|
|
390
|
-
<%
|
|
411
|
+
<% } %>
|
|
391
412
|
} catch (error) {
|
|
392
413
|
logger.error('Google callback error:', error);
|
|
393
414
|
res.redirect('/login?error=social_auth_failed');
|
|
@@ -398,19 +419,28 @@ class AuthController {
|
|
|
398
419
|
<% if (socialAuth.includes('GitHub')) { -%>
|
|
399
420
|
async githubLogin(req, res) {
|
|
400
421
|
const rootUrl = 'https://github.com/login/oauth/authorize';
|
|
422
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
423
|
+
this.setOAuthStateCookie(res, state);
|
|
424
|
+
|
|
401
425
|
const options = {
|
|
402
426
|
client_id: process.env.GITHUB_CLIENT_ID,
|
|
403
427
|
redirect_uri: process.env.GITHUB_CALLBACK_URL || 'http://localhost:3000/api/auth/github/callback',
|
|
404
428
|
scope: 'user:email',
|
|
405
|
-
state:
|
|
429
|
+
state: state
|
|
406
430
|
};
|
|
407
431
|
const qs = new URLSearchParams(options);
|
|
408
432
|
res.redirect(`${rootUrl}?${qs.toString()}`);
|
|
409
433
|
}
|
|
410
434
|
|
|
411
|
-
async githubCallback(req, res,
|
|
435
|
+
async githubCallback(req, res, _next) {
|
|
412
436
|
try {
|
|
413
|
-
const { code } = req.query;
|
|
437
|
+
const { code, state } = req.query;
|
|
438
|
+
const savedState = req.cookies?.oauth_state;
|
|
439
|
+
res.clearCookie('oauth_state');
|
|
440
|
+
|
|
441
|
+
if (!state || state !== savedState) {
|
|
442
|
+
return res.status(HTTP_STATUS.FORBIDDEN).json({ message: 'Invalid state parameter' });
|
|
443
|
+
}
|
|
414
444
|
|
|
415
445
|
<%_ if (architecture === 'Clean Architecture') { _%>
|
|
416
446
|
const useCase = new SocialLoginUseCase(new GitHubProvider(), new UserRepository());
|
|
@@ -428,7 +458,7 @@ class AuthController {
|
|
|
428
458
|
const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
429
459
|
activeTokens.push(refreshJti);
|
|
430
460
|
JwtService.activeRefreshTokens.set(userId, activeTokens);
|
|
431
|
-
<%
|
|
461
|
+
<% } %>
|
|
432
462
|
|
|
433
463
|
res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
|
|
434
464
|
res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
|
|
@@ -466,7 +496,7 @@ class AuthController {
|
|
|
466
496
|
const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
467
497
|
activeTokens.push(refreshJti);
|
|
468
498
|
JwtService.activeRefreshTokens.set(userId, activeTokens);
|
|
469
|
-
<%
|
|
499
|
+
<% } %>
|
|
470
500
|
|
|
471
501
|
res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
|
|
472
502
|
res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
|