nodejs-quickstart-structure 2.1.2 → 2.2.1
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 +20 -0
- package/README.md +12 -17
- package/bin/index.js +1 -0
- package/lib/generator.js +1 -1
- package/lib/modules/app-setup.js +16 -0
- package/lib/modules/auth-setup.js +46 -4
- package/lib/prompts.js +49 -5
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -2
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +27 -0
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +24 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +5 -1
- package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -2
- package/templates/clean-architecture/ts/src/domain/user.ts.ejs +14 -0
- package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
- package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
- package/templates/common/.env.example.ejs +10 -0
- package/templates/common/README.md.ejs +65 -14
- package/templates/common/auth/js/controllers/authController.js.ejs +356 -13
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +329 -53
- package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
- package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
- package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
- package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
- package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +192 -0
- package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
- package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
- package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +366 -64
- package/templates/common/auth/ts/controllers/authController.ts.ejs +370 -9
- package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
- package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
- package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
- package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
- package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
- package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
- package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
- package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
- package/templates/common/database/js/models/User.js.ejs +13 -5
- package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
- package/templates/common/database/ts/models/User.ts.ejs +23 -7
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
- package/templates/common/docker-compose.yml.ejs +21 -0
- package/templates/common/ecosystem.config.js.ejs +10 -0
- package/templates/common/eslint.config.mjs.ejs +4 -1
- package/templates/common/jest.config.js.ejs +1 -1
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
- package/templates/common/package.json.ejs +4 -0
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
- package/templates/common/swagger.yml.ejs +62 -3
- package/templates/common/views/ejs/login.ejs.ejs +84 -0
- package/templates/common/views/ejs/signup.ejs.ejs +84 -0
- package/templates/common/views/pug/login.pug.ejs +78 -0
- package/templates/common/views/pug/signup.pug.ejs +78 -0
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
- package/templates/mvc/js/src/config/env.js.ejs +12 -2
- package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
- 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/config/env.ts.ejs +12 -2
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
- package/templates/mvc/ts/src/index.ts.ejs +4 -1
- package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
- package/templates/clean-architecture/ts/src/domain/user.ts +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
2
2
|
|
|
3
|
+
## [2.2.1] - 2026-05-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Secure OAuth CSRF Protection**: Implemented robust state-validation using cryptographically secure random tokens and `HttpOnly` cookies to mitigate CSRF attacks in social authentication flows.
|
|
7
|
+
- **Improved CLI Robustness**: Enhanced argument parsing to support comma-separated strings for variadic flags like `--social-auth` and `--auth`.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Architectural Parity & Type Safety**: Standardized `HTTP_STATUS.FORBIDDEN` across all templates and resolved TypeScript type inference issues (`never` type) in generated Clean Architecture controllers.
|
|
11
|
+
|
|
12
|
+
## [2.2.0] - 2026-05-05
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **Social Login Support**: Integrated Google and GitHub authentication into the Clean Architecture and MVC templates.
|
|
16
|
+
- **Expanded Validation Matrix**: Scaled the mathematical validation matrix to **7,920+ unique project scenarios**, ensuring 100% template rendering accuracy across all permutations.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Clean Architecture Restructuring**: Moved use cases from `src/domain/usecases` to `src/usecases` (Application Layer) to strictly follow architectural boundaries.
|
|
20
|
+
- **Improved EJS Alignment**: Optimized all templates with whitespace-slurping tags (`<%_`, `_%>`) to eliminate redundant blank lines in generated code.
|
|
21
|
+
- **Environment Validation**: Added social authentication credentials to Zod-based environment schemas.
|
|
22
|
+
|
|
3
23
|
## [2.1.2] - 2026-04-27
|
|
4
24
|
|
|
5
25
|
### Changed
|
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
|
+
- [7,920+ Project Combinations](#7920-project-combinations)
|
|
21
21
|
- [Configuration Options](#configuration-options)
|
|
22
22
|
- [Generated Project Structure](#generated-project-structure)
|
|
23
23
|
- [Documentation](#documentation)
|
|
@@ -50,20 +50,15 @@ nodejs-quickstart init
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
-
## What's New in v2.
|
|
53
|
+
## What's New in v2.2 (The Social Auth Release)
|
|
54
54
|
|
|
55
|
-
The v2.
|
|
55
|
+
The v2.2.0 release brings enterprise-grade identity management to your microservices:
|
|
56
56
|
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **Next-Gen Web UI**: A browser-based visual project simulator with real-time folder previews.
|
|
60
|
-
- **Enterprise Clean Architecture**: High-fidelity structure for professional Microservices (TS/JS).
|
|
61
|
-
- **Hardened Security**: Integrated Snyk & SonarCloud logic in core templates.
|
|
62
|
-
- **Zero-Prompt Workflow**: Generate projects with a single CLI command.
|
|
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.
|
|
63
59
|
|
|
64
60
|
---
|
|
65
61
|
|
|
66
|
-
|
|
67
62
|
## Key Features
|
|
68
63
|
|
|
69
64
|
- **Interactive CLI**: Smooth, guided configuration process.
|
|
@@ -72,7 +67,7 @@ nodejs-quickstart init
|
|
|
72
67
|
- **Database Ready**: Pre-configured for **MySQL**, **PostgreSQL**, or **MongoDB**.
|
|
73
68
|
- **Communication Patterns**: Supports **REST**, **GraphQL** (Apollo), and **Kafka** (Event-driven).
|
|
74
69
|
- **Multi-layer Caching**: Integrated **Redis** or built-in **Memory Cache**.
|
|
75
|
-
- **Pluggable Authentication**: Built-in **JWT**
|
|
70
|
+
- **Pluggable Authentication**: Built-in **JWT** and **OAuth2 (Google/GitHub)** support with Access/Refresh token rotation.
|
|
76
71
|
- **AI-Native Optimized**: specifically designed for **Cursor** and AI agents, including built-in `.cursorrules` and Agent Skill prompts.
|
|
77
72
|
|
|
78
73
|
---
|
|
@@ -90,14 +85,14 @@ We don't just generate boilerplate; we generate **production-ready** foundations
|
|
|
90
85
|
|
|
91
86
|
---
|
|
92
87
|
|
|
93
|
-
##
|
|
88
|
+
## 7,920+ Project Combinations
|
|
94
89
|
|
|
95
90
|
The CLI supports a massive number of configurations to fit your exact needs:
|
|
96
91
|
|
|
97
|
-
- **
|
|
98
|
-
- **MVC Architecture**:
|
|
99
|
-
- **Clean Architecture**:
|
|
100
|
-
- **
|
|
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**:
|
|
101
96
|
- Every combination can be generated across 5 CI/CD providers.
|
|
102
97
|
- Optional **Enterprise-Grade Security Hardening** doubles the scenarios.
|
|
103
98
|
- Every single scenario is verified to be compatible with our **80% Coverage Threshold** policy.
|
|
@@ -114,7 +109,7 @@ The CLI will guide you through:
|
|
|
114
109
|
5. **Database**: `MySQL` | `PostgreSQL` | `MongoDB`
|
|
115
110
|
6. **Communication**: `REST` | `GraphQL` | `Kafka`
|
|
116
111
|
7. **Caching**: `None` | `Redis` | `Memory Cache`
|
|
117
|
-
8. **Auth**: `None` | `JWT`
|
|
112
|
+
8. **Auth**: `None` | `JWT` | `OAuth2 (Google/GitHub) + JWT`
|
|
118
113
|
9. **CI/CD**: `GitHub Actions` | `Jenkins` | `GitLab CI` | `CircleCI` | `Bitbucket Pipelines`
|
|
119
114
|
10. **Security**: (Optional) Snyk & SonarCloud Hardening
|
|
120
115
|
|
package/bin/index.js
CHANGED
|
@@ -34,6 +34,7 @@ program
|
|
|
34
34
|
.option('--no-include-security', 'Exclude Enterprise Security Hardening')
|
|
35
35
|
.option('--caching <type>', 'Caching Layer (None/Redis/Memory Cache)')
|
|
36
36
|
.option('--auth <modes...>', 'Authentication Modes (None, JWT)')
|
|
37
|
+
.option('--social-auth <providers...>', 'Social Authentication Providers (None, Google, GitHub)')
|
|
37
38
|
.option('--advanced-options', 'Enable Advanced Options')
|
|
38
39
|
.option('--no-advanced-options', 'Disable Advanced Options')
|
|
39
40
|
.action(async (options) => {
|
package/lib/generator.js
CHANGED
|
@@ -112,7 +112,7 @@ export const generateProject = async (config) => {
|
|
|
112
112
|
Language: ${language}
|
|
113
113
|
Database: ${config.database}
|
|
114
114
|
Communication: ${config.communication}${config.caching && config.caching !== 'None' ? `\n Caching: ${config.caching}` : ''}
|
|
115
|
-
Authentication: ${config.auth.join('
|
|
115
|
+
Authentication: ${[...(config.socialAuth || []).filter(s => s !== 'None'), ...config.auth.filter(a => a !== 'None')].join(' - ') || 'None'}
|
|
116
116
|
|
|
117
117
|
----------------------------------------------------
|
|
118
118
|
✨ High-Quality Standards Applied:
|
package/lib/modules/app-setup.js
CHANGED
|
@@ -175,6 +175,22 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
|
|
|
175
175
|
await fs.remove(path.join(targetDir, 'src/interfaces/routes', `${routeName}.ejs`));
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// --- Render All Domain Entities ---
|
|
179
|
+
const domainDir = path.join(targetDir, 'src/domain');
|
|
180
|
+
if (await fs.pathExists(domainDir)) {
|
|
181
|
+
const domainFiles = await fs.readdir(domainDir);
|
|
182
|
+
for (const file of domainFiles) {
|
|
183
|
+
if (file.endsWith('.ejs')) {
|
|
184
|
+
const templatePathObj = path.join(domainDir, file);
|
|
185
|
+
const destPathObj = path.join(domainDir, file.replace('.ejs', ''));
|
|
186
|
+
const template = await fs.readFile(templatePathObj, 'utf-8');
|
|
187
|
+
const content = ejs.render(template, { ...config });
|
|
188
|
+
await fs.writeFile(destPathObj, content);
|
|
189
|
+
await fs.remove(templatePathObj);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
// --- Render All Use Cases ---
|
|
179
195
|
const useCaseDir = path.join(targetDir, 'src/usecases');
|
|
180
196
|
if (await fs.pathExists(useCaseDir)) {
|
|
@@ -13,6 +13,10 @@ export const setupAuth = async (templatesDir, targetDir, config) => {
|
|
|
13
13
|
// 1. JWT Service
|
|
14
14
|
await renderAuthComponent(authSource, targetDir, 'services', `jwtService.${langExt}`, config);
|
|
15
15
|
|
|
16
|
+
if (config.socialAuth && config.socialAuth.filter(a => a !== 'None').length > 0) {
|
|
17
|
+
await renderAuthComponent(authSource, targetDir, 'services', `socialAuthService.${langExt}`, config);
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
// 2. Auth Middleware
|
|
17
21
|
await renderAuthComponent(authSource, targetDir, 'middleware', `authMiddleware.${langExt}`, config);
|
|
18
22
|
|
|
@@ -22,7 +26,12 @@ export const setupAuth = async (templatesDir, targetDir, config) => {
|
|
|
22
26
|
// 4. Auth Routes
|
|
23
27
|
await renderAuthComponent(authSource, targetDir, 'routes', `authRoutes.${langExt}`, config);
|
|
24
28
|
|
|
25
|
-
// 5.
|
|
29
|
+
// 5. Social Login Use Case (Clean Architecture only)
|
|
30
|
+
if (architecture === 'Clean Architecture' && config.socialAuth && config.socialAuth.filter(a => a !== 'None').length > 0) {
|
|
31
|
+
await renderAuthComponent(authSource, targetDir, 'usecases', `SocialLoginUseCase.${langExt}`, config);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 6. MVC Views (if applicable)
|
|
26
35
|
if (architecture === 'MVC' && viewEngine && viewEngine !== 'None') {
|
|
27
36
|
const engine = viewEngine.toLowerCase();
|
|
28
37
|
const views = ['login', 'signup'];
|
|
@@ -37,7 +46,7 @@ export const setupAuth = async (templatesDir, targetDir, config) => {
|
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
//
|
|
49
|
+
// 7. Restructuring for Clean Architecture
|
|
41
50
|
if (architecture === 'Clean Architecture') {
|
|
42
51
|
await restructureAuthForCleanArch(targetDir, langExt);
|
|
43
52
|
}
|
|
@@ -82,6 +91,21 @@ async function restructureAuthForCleanArch(targetDir, ext) {
|
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
if (await fs.pathExists(path.join(targetDir, `src/services/socialAuthService.${ext}`))) {
|
|
95
|
+
await fs.move(
|
|
96
|
+
path.join(targetDir, `src/services/socialAuthService.${ext}`),
|
|
97
|
+
path.join(targetDir, `src/infrastructure/auth/socialAuthService.${ext}`),
|
|
98
|
+
{ overwrite: true }
|
|
99
|
+
);
|
|
100
|
+
if (await fs.pathExists(path.join(targetDir, `tests/unit/services/socialAuthService.spec.${ext}`))) {
|
|
101
|
+
await fs.move(
|
|
102
|
+
path.join(targetDir, `tests/unit/services/socialAuthService.spec.${ext}`),
|
|
103
|
+
path.join(targetDir, `tests/unit/infrastructure/auth/socialAuthService.spec.${ext}`),
|
|
104
|
+
{ overwrite: true }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
// Controllers -> Interfaces/Controllers/auth
|
|
86
110
|
await fs.ensureDir(path.join(targetDir, 'src/interfaces/controllers/auth'));
|
|
87
111
|
await fs.ensureDir(path.join(targetDir, 'tests/unit/interfaces/controllers/auth'));
|
|
@@ -128,10 +152,28 @@ async function restructureAuthForCleanArch(targetDir, ext) {
|
|
|
128
152
|
);
|
|
129
153
|
}
|
|
130
154
|
|
|
155
|
+
// UseCases -> UseCases/auth
|
|
156
|
+
if (await fs.pathExists(path.join(targetDir, `src/usecases/SocialLoginUseCase.${ext}`))) {
|
|
157
|
+
await fs.ensureDir(path.join(targetDir, 'src/usecases/auth'));
|
|
158
|
+
await fs.ensureDir(path.join(targetDir, 'tests/unit/usecases/auth'));
|
|
159
|
+
await fs.move(
|
|
160
|
+
path.join(targetDir, `src/usecases/SocialLoginUseCase.${ext}`),
|
|
161
|
+
path.join(targetDir, `src/usecases/auth/socialLoginUseCase.${ext}`),
|
|
162
|
+
{ overwrite: true }
|
|
163
|
+
);
|
|
164
|
+
if (await fs.pathExists(path.join(targetDir, `tests/unit/usecases/SocialLoginUseCase.spec.${ext}`))) {
|
|
165
|
+
await fs.move(
|
|
166
|
+
path.join(targetDir, `tests/unit/usecases/SocialLoginUseCase.spec.${ext}`),
|
|
167
|
+
path.join(targetDir, `tests/unit/usecases/auth/socialLoginUseCase.spec.${ext}`),
|
|
168
|
+
{ overwrite: true }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
131
173
|
// Cleanup empty dirs in src and tests/unit
|
|
132
174
|
const dirsToCleanup = [
|
|
133
|
-
'src/services', 'src/controllers', 'src/middleware', 'src/routes',
|
|
134
|
-
'tests/unit/services', 'tests/unit/controllers', 'tests/unit/middleware'
|
|
175
|
+
'src/services', 'src/controllers', 'src/middleware', 'src/routes', 'src/usecases',
|
|
176
|
+
'tests/unit/services', 'tests/unit/controllers', 'tests/unit/middleware', 'tests/unit/usecases'
|
|
135
177
|
];
|
|
136
178
|
for (const dir of dirsToCleanup) {
|
|
137
179
|
const fullDir = path.join(targetDir, dir);
|
package/lib/prompts.js
CHANGED
|
@@ -96,9 +96,9 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
type: 'select',
|
|
99
|
-
name: '
|
|
99
|
+
name: 'authChoice',
|
|
100
100
|
message: 'Select Authentication Mode:',
|
|
101
|
-
choices: ['None', 'JWT'],
|
|
101
|
+
choices: ['None', 'JWT', 'Google - Github - JWT'],
|
|
102
102
|
default: 'None',
|
|
103
103
|
when: (answers) => {
|
|
104
104
|
const advanced = options.advancedOptions !== undefined ? options.advancedOptions : answers.advancedOptions;
|
|
@@ -119,9 +119,37 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
119
119
|
result.advancedOptions = result.advancedOptions === 'Yes';
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
//
|
|
122
|
+
// Map authChoice to auth and socialAuth if not provided via options
|
|
123
|
+
if (!options.auth && result.authChoice) {
|
|
124
|
+
const choice = result.authChoice.toLowerCase();
|
|
125
|
+
if (choice.includes('google') || choice.includes('github')) {
|
|
126
|
+
result.auth = ['JWT'];
|
|
127
|
+
result.socialAuth = ['Google', 'GitHub'];
|
|
128
|
+
} else if (choice.includes('jwt')) {
|
|
129
|
+
result.auth = ['JWT'];
|
|
130
|
+
result.socialAuth = ['None'];
|
|
131
|
+
} else {
|
|
132
|
+
result.auth = ['None'];
|
|
133
|
+
result.socialAuth = ['None'];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Cleanup the temporary authChoice
|
|
138
|
+
delete result.authChoice;
|
|
139
|
+
|
|
140
|
+
// Normalize auth to array if it's a string from the options
|
|
123
141
|
if (typeof result.auth === 'string') {
|
|
124
|
-
result.auth =
|
|
142
|
+
result.auth = result.auth.split(',').map(s => s.trim());
|
|
143
|
+
} else if (Array.isArray(result.auth)) {
|
|
144
|
+
result.auth = result.auth.flatMap(s => s.split(',').map(ss => ss.trim()));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Map friendly CLI strings to actual values
|
|
148
|
+
if (result.auth && (result.auth.includes('Google - Github - JWT') || result.auth.includes('Google - GitHub - JWT'))) {
|
|
149
|
+
result.auth = ['JWT'];
|
|
150
|
+
if (!result.socialAuth || result.socialAuth.includes('None')) {
|
|
151
|
+
result.socialAuth = ['Google', 'GitHub'];
|
|
152
|
+
}
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
// Default auth if not provided
|
|
@@ -134,6 +162,22 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
134
162
|
result.auth = result.auth.filter(a => a !== 'None');
|
|
135
163
|
}
|
|
136
164
|
|
|
165
|
+
// Normalize socialAuth to array from options
|
|
166
|
+
if (typeof result.socialAuth === 'string') {
|
|
167
|
+
result.socialAuth = result.socialAuth.split(',').map(s => s.trim());
|
|
168
|
+
} else if (Array.isArray(result.socialAuth)) {
|
|
169
|
+
result.socialAuth = result.socialAuth.flatMap(s => s.split(',').map(ss => ss.trim()));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Default socialAuth if not provided
|
|
173
|
+
if (!result.socialAuth) {
|
|
174
|
+
result.socialAuth = ['None'];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Filter out 'None' if providers are selected
|
|
178
|
+
if (result.socialAuth.includes('Google') || result.socialAuth.includes('GitHub') || result.socialAuth.includes('Github')) {
|
|
179
|
+
result.socialAuth = result.socialAuth.filter(a => a !== 'None');
|
|
180
|
+
}
|
|
181
|
+
|
|
137
182
|
return result;
|
|
138
183
|
};
|
|
139
|
-
|
package/package.json
CHANGED
|
@@ -33,12 +33,22 @@ const envSchema = z.object({
|
|
|
33
33
|
<%_ if (communication === 'Kafka') { -%>
|
|
34
34
|
KAFKA_BROKER: z.string(),
|
|
35
35
|
<%_ } -%>
|
|
36
|
-
<%_ if (auth.includes('JWT')) {
|
|
36
|
+
<%_ if (auth.includes('JWT')) { _%>
|
|
37
37
|
JWT_SECRET: z.string(),
|
|
38
38
|
JWT_EXPIRES_IN: z.string().default('15m'),
|
|
39
39
|
JWT_REFRESH_SECRET: z.string().default('your-secret-refresh-key'),
|
|
40
40
|
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
|
|
41
|
-
<%_
|
|
41
|
+
<%_ if (socialAuth && socialAuth.includes('Google')) { _%>
|
|
42
|
+
GOOGLE_CLIENT_ID: z.string(),
|
|
43
|
+
GOOGLE_CLIENT_SECRET: z.string(),
|
|
44
|
+
GOOGLE_CALLBACK_URL: z.string().optional(),
|
|
45
|
+
<%_ } _%>
|
|
46
|
+
<%_ if (socialAuth && socialAuth.includes('GitHub')) { _%>
|
|
47
|
+
GITHUB_CLIENT_ID: z.string(),
|
|
48
|
+
GITHUB_CLIENT_SECRET: z.string(),
|
|
49
|
+
GITHUB_CALLBACK_URL: z.string().optional(),
|
|
50
|
+
<%_ } _%>
|
|
51
|
+
<%_ } _%>
|
|
42
52
|
});
|
|
43
53
|
|
|
44
54
|
const _env = envSchema.safeParse(process.env);
|
package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs
CHANGED
|
@@ -38,6 +38,33 @@ class UserRepository {
|
|
|
38
38
|
<%_ } -%>
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
async findByEmail(email) {
|
|
42
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
43
|
+
const user = await UserModel.findOne({ email });
|
|
44
|
+
if (!user) return null;
|
|
45
|
+
return {
|
|
46
|
+
id: user._id.toString(),
|
|
47
|
+
name: user.name,
|
|
48
|
+
email: user.email,
|
|
49
|
+
googleId: user.googleId,
|
|
50
|
+
githubId: user.githubId
|
|
51
|
+
};
|
|
52
|
+
<%_ } else if (database === 'None') { -%>
|
|
53
|
+
return await UserModel.findOne({ email });
|
|
54
|
+
<%_ } else { -%>
|
|
55
|
+
const user = await UserModel.findOne({ where: { email } });
|
|
56
|
+
if (!user) return null;
|
|
57
|
+
return {
|
|
58
|
+
id: user.id,
|
|
59
|
+
name: user.name,
|
|
60
|
+
email: user.email,
|
|
61
|
+
googleId: user.googleId,
|
|
62
|
+
githubId: user.githubId
|
|
63
|
+
};
|
|
64
|
+
<%_ } -%>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
41
68
|
async getUsers() {
|
|
42
69
|
<%_ if (database === 'None') { -%>
|
|
43
70
|
return await UserModel.find();
|
package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs
CHANGED
|
@@ -5,6 +5,7 @@ jest.mock('@/infrastructure/database/models/User', () => ({
|
|
|
5
5
|
create: jest.fn(),
|
|
6
6
|
findAll: jest.fn(),
|
|
7
7
|
findByPk: jest.fn(),
|
|
8
|
+
findOne: jest.fn(),
|
|
8
9
|
update: jest.fn(),
|
|
9
10
|
destroy: jest.fn(),
|
|
10
11
|
find: jest.fn(),
|
|
@@ -83,6 +84,29 @@ describe('UserRepository', () => {
|
|
|
83
84
|
});
|
|
84
85
|
});
|
|
85
86
|
|
|
87
|
+
describe('findByEmail', () => {
|
|
88
|
+
it('should find and return a user by email', async () => {
|
|
89
|
+
const email = 'test@example.com';
|
|
90
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
91
|
+
const mockDbRecord = { _id: '1', name: 'TestUser', email: 'test@example.com' };
|
|
92
|
+
<%_ } else { -%>
|
|
93
|
+
const mockDbRecord = { id: '1', name: 'TestUser', email: 'test@example.com' };
|
|
94
|
+
<%_ } -%>
|
|
95
|
+
UserModel.findOne.mockResolvedValue(mockDbRecord);
|
|
96
|
+
|
|
97
|
+
const result = await userRepository.findByEmail(email);
|
|
98
|
+
|
|
99
|
+
expect(result.email).toBe(email);
|
|
100
|
+
expect(UserModel.findOne).toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return null if email not found', async () => {
|
|
104
|
+
UserModel.findOne.mockResolvedValue(null);
|
|
105
|
+
const result = await userRepository.findByEmail('not@found.com');
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
86
110
|
describe('getUsers', () => {
|
|
87
111
|
it('should return a list of mapped UserEntities (Happy Path)', async () => {
|
|
88
112
|
<%_ if (database === 'MongoDB') { -%>
|
|
@@ -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,12 +31,15 @@ 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') { -%>
|
|
36
38
|
app.use('/api', apiRoutes);
|
|
37
39
|
<%_ } -%>
|
|
38
|
-
<%_ if (auth.includes('JWT')) { -%>
|
|
40
|
+
<%_ if (auth.includes('JWT')) { -%>
|
|
41
|
+
app.use('/api/auth', authRoutes);
|
|
42
|
+
<%_ } -%>
|
|
39
43
|
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
40
44
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
41
45
|
<%_ } -%>
|
|
@@ -34,12 +34,22 @@ const envSchema = z.object({
|
|
|
34
34
|
<%_ if (communication === 'Kafka') { -%>
|
|
35
35
|
KAFKA_BROKER: z.string(),
|
|
36
36
|
<%_ } -%>
|
|
37
|
-
<%_ if (auth.includes('JWT')) {
|
|
37
|
+
<%_ if (auth.includes('JWT')) { _%>
|
|
38
38
|
JWT_SECRET: z.string(),
|
|
39
39
|
JWT_EXPIRES_IN: z.string().default('15m'),
|
|
40
40
|
JWT_REFRESH_SECRET: z.string().default('your-secret-refresh-key'),
|
|
41
41
|
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
|
|
42
|
-
<%_
|
|
42
|
+
<%_ if (socialAuth && socialAuth.includes('Google')) { _%>
|
|
43
|
+
GOOGLE_CLIENT_ID: z.string(),
|
|
44
|
+
GOOGLE_CLIENT_SECRET: z.string(),
|
|
45
|
+
GOOGLE_CALLBACK_URL: z.string().optional(),
|
|
46
|
+
<%_ } _%>
|
|
47
|
+
<%_ if (socialAuth && socialAuth.includes('GitHub')) { _%>
|
|
48
|
+
GITHUB_CLIENT_ID: z.string(),
|
|
49
|
+
GITHUB_CLIENT_SECRET: z.string(),
|
|
50
|
+
GITHUB_CALLBACK_URL: z.string().optional(),
|
|
51
|
+
<%_ } _%>
|
|
52
|
+
<%_ } _%>
|
|
43
53
|
});
|
|
44
54
|
|
|
45
55
|
const _env = envSchema.safeParse(process.env);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class User {
|
|
2
|
+
constructor(
|
|
3
|
+
public id: number | string | null,
|
|
4
|
+
public name: string,
|
|
5
|
+
public email: string,
|
|
6
|
+
public password?: string | null,
|
|
7
|
+
<%_ if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('Google')) { _%>
|
|
8
|
+
public googleId?: string | null,
|
|
9
|
+
<%_ } _%>
|
|
10
|
+
<%_ if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('GitHub')) { _%>
|
|
11
|
+
public githubId?: string | null,
|
|
12
|
+
<%_ } _%>
|
|
13
|
+
) { }
|
|
14
|
+
}
|
|
@@ -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
|
|
package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs
CHANGED
|
@@ -9,6 +9,7 @@ jest.mock('@/infrastructure/database/models/User', () => {
|
|
|
9
9
|
create: jest.fn(),
|
|
10
10
|
findAll: jest.fn(),
|
|
11
11
|
findByPk: jest.fn(),
|
|
12
|
+
findOne: jest.fn(),
|
|
12
13
|
update: jest.fn(),
|
|
13
14
|
destroy: jest.fn(),
|
|
14
15
|
find: jest.fn(),
|
|
@@ -115,6 +116,29 @@ describe('UserRepository', () => {
|
|
|
115
116
|
});
|
|
116
117
|
});
|
|
117
118
|
|
|
119
|
+
describe('findByEmail', () => {
|
|
120
|
+
it('should find and return a user by email', async () => {
|
|
121
|
+
const email = 'test@example.com';
|
|
122
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
123
|
+
const mockDbRecord = { _id: { toString: () => '1' }, name: 'TestUser', email: 'test@example.com' };
|
|
124
|
+
<%_ } else { -%>
|
|
125
|
+
const mockDbRecord = { id: '1', name: 'TestUser', email: 'test@example.com' };
|
|
126
|
+
<%_ } -%>
|
|
127
|
+
(UserModel.findOne as jest.Mock).mockResolvedValue(mockDbRecord);
|
|
128
|
+
|
|
129
|
+
const result = await userRepository.findByEmail(email);
|
|
130
|
+
|
|
131
|
+
expect(result?.email).toBe(email);
|
|
132
|
+
expect(UserModel.findOne).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return null if email not found', async () => {
|
|
136
|
+
(UserModel.findOne as jest.Mock).mockResolvedValue(null);
|
|
137
|
+
const result = await userRepository.findByEmail('not@found.com');
|
|
138
|
+
expect(result).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
118
142
|
describe('getUsers', () => {
|
|
119
143
|
it('should return a list of mapped UserEntities (Happy Path)', async () => {
|
|
120
144
|
// Arrange
|