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.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +14 -13
  3. package/bin/index.js +2 -1
  4. package/lib/generator.js +10 -1
  5. package/lib/modules/project-setup.js +41 -0
  6. package/lib/modules/terraform-setup.js +131 -0
  7. package/lib/prompts.js +22 -3
  8. package/package.json +1 -1
  9. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +2 -0
  10. package/templates/clean-architecture/js/src/usecases/CreateUser.js.ejs +5 -2
  11. package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
  12. package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +4 -2
  13. package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
  14. package/templates/common/auth/js/controllers/authController.js.ejs +42 -12
  15. package/templates/common/auth/js/controllers/authController.spec.js.ejs +95 -5
  16. package/templates/common/auth/js/services/jwtService.js.ejs +3 -3
  17. package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +0 -2
  18. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +29 -7
  19. package/templates/common/auth/ts/controllers/authController.ts.ejs +34 -5
  20. package/templates/common/caching/clean/js/CreateUser.js.ejs +4 -2
  21. package/templates/common/caching/clean/ts/createUser.ts.ejs +4 -2
  22. package/templates/common/eslint.config.mjs.ejs +4 -1
  23. package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
  24. package/templates/common/package.json.ejs +2 -0
  25. package/templates/common/terraform/main.tf +52 -0
  26. package/templates/common/terraform/modules/cache/main.tf +41 -0
  27. package/templates/common/terraform/modules/cache/outputs.tf +7 -0
  28. package/templates/common/terraform/modules/cache/variables.tf +4 -0
  29. package/templates/common/terraform/modules/compute/main.tf +69 -0
  30. package/templates/common/terraform/modules/compute/outputs.tf +7 -0
  31. package/templates/common/terraform/modules/compute/variables.tf +20 -0
  32. package/templates/common/terraform/modules/database/main.tf +57 -0
  33. package/templates/common/terraform/modules/database/outputs.tf +16 -0
  34. package/templates/common/terraform/modules/database/variables.tf +27 -0
  35. package/templates/common/terraform/modules/security/main.tf +130 -0
  36. package/templates/common/terraform/modules/security/outputs.tf +15 -0
  37. package/templates/common/terraform/modules/security/variables.tf +12 -0
  38. package/templates/common/terraform/modules/vpc/main.tf +134 -0
  39. package/templates/common/terraform/modules/vpc/outputs.tf +19 -0
  40. package/templates/common/terraform/modules/vpc/variables.tf +45 -0
  41. package/templates/common/terraform/outputs.tf +29 -0
  42. package/templates/common/terraform/provider.tf +17 -0
  43. package/templates/common/terraform/variables.tf +33 -0
  44. package/templates/mvc/js/src/index.js.ejs +2 -0
  45. package/templates/mvc/js/src/utils/httpCodes.js +1 -0
  46. package/templates/mvc/ts/src/index.ts.ejs +2 -0
  47. 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
- - [7,920+ Project Combinations](#7920-project-combinations)
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://paudang.github.io/nodejs-quickstart-structure/#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://paudang.github.io/nodejs-quickstart-structure/#configurator) | [See CLI Commands ↓](#path-b-interactive-cli) |
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.2 (The Social Auth Release)
53
+ ## What's New in v2.3 (The Infrastructure & Terraform Release)
54
54
 
55
- The v2.2.0 release brings enterprise-grade identity management to your microservices:
55
+ The v2.3.0 release brings professional-grade Infrastructure as Code (IaC) to your generated projects:
56
56
 
57
- - **OAuth2 Social Login**: Seamlessly integrate **Google** and **GitHub** authentication with automatic user provisioning and JWT session linking.
58
- - **Massive Matrix Expansion**: Now supporting **7,920+ unique project scenarios**, mathematically validated for template consistency.
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
- ## 7,920+ Project Combinations
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
- - **720 Core Combinations**:
93
- - **MVC Architecture**: 540 variants (Languages × View Engines × Databases × Communication Patterns × Caching × Auth)
94
- - **Clean Architecture**: 180 variants (Languages × Databases × Communication Patterns × Caching × Auth)
95
- - **7,920+ Total Scenarios**:
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://paudang.github.io/nodejs-quickstart-structure/guide/getting-started.html)**.
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 = [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 = [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,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-quickstart-structure",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "The ultimate nodejs quickstart structure CLI to scaffold Node.js microservices with MVC or Clean Architecture",
6
6
  "main": "bin/index.js",
@@ -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 {
@@ -3,6 +3,7 @@ export const HTTP_STATUS = {
3
3
  CREATED: 201,
4
4
  BAD_REQUEST: 400,
5
5
  UNAUTHORIZED: 401,
6
+ FORBIDDEN: 403,
6
7
  NOT_FOUND: 404,
7
8
  INTERNAL_SERVER_ERROR: 500
8
9
  } as const;
@@ -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: 'google'
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, next) {
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: 'github'
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, next) {
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' });