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.
Files changed (70) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +12 -17
  3. package/bin/index.js +1 -0
  4. package/lib/generator.js +1 -1
  5. package/lib/modules/app-setup.js +16 -0
  6. package/lib/modules/auth-setup.js +46 -4
  7. package/lib/prompts.js +49 -5
  8. package/package.json +1 -1
  9. package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -2
  10. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +27 -0
  11. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +24 -0
  12. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +5 -1
  13. package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -2
  14. package/templates/clean-architecture/ts/src/domain/user.ts.ejs +14 -0
  15. package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
  16. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
  17. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
  18. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  19. package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
  20. package/templates/common/.env.example.ejs +10 -0
  21. package/templates/common/README.md.ejs +65 -14
  22. package/templates/common/auth/js/controllers/authController.js.ejs +356 -13
  23. package/templates/common/auth/js/controllers/authController.spec.js.ejs +329 -53
  24. package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
  25. package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
  26. package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
  27. package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
  28. package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +192 -0
  29. package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
  30. package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
  31. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +366 -64
  32. package/templates/common/auth/ts/controllers/authController.ts.ejs +370 -9
  33. package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
  34. package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
  35. package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
  36. package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
  37. package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
  38. package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
  39. package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
  40. package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
  41. package/templates/common/database/js/models/User.js.ejs +13 -5
  42. package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
  43. package/templates/common/database/ts/models/User.ts.ejs +23 -7
  44. package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
  45. package/templates/common/docker-compose.yml.ejs +21 -0
  46. package/templates/common/ecosystem.config.js.ejs +10 -0
  47. package/templates/common/eslint.config.mjs.ejs +4 -1
  48. package/templates/common/jest.config.js.ejs +1 -1
  49. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
  50. package/templates/common/package.json.ejs +4 -0
  51. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
  52. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
  53. package/templates/common/swagger.yml.ejs +62 -3
  54. package/templates/common/views/ejs/login.ejs.ejs +84 -0
  55. package/templates/common/views/ejs/signup.ejs.ejs +84 -0
  56. package/templates/common/views/pug/login.pug.ejs +78 -0
  57. package/templates/common/views/pug/signup.pug.ejs +78 -0
  58. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
  59. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
  60. package/templates/mvc/js/src/config/env.js.ejs +12 -2
  61. package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
  62. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
  63. package/templates/mvc/js/src/index.js.ejs +2 -0
  64. package/templates/mvc/js/src/utils/httpCodes.js +1 -0
  65. package/templates/mvc/ts/src/config/env.ts.ejs +12 -2
  66. package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
  67. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  68. package/templates/mvc/ts/src/index.ts.ejs +4 -1
  69. package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
  70. 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
- - [5,280+ Project Combinations](#5280-project-combinations)
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.1 (The Authentication Release)
53
+ ## What's New in v2.2 (The Social Auth Release)
54
54
 
55
- The v2.1.0 release is a major leap forward, turning the generator into a **Community Standard**:
55
+ The v2.2.0 release brings enterprise-grade identity management to your microservices:
56
56
 
57
- - **Pluggable JWT Authentication**: Production-ready access & refresh token patterns with automatic PM2/Environment configuration.
58
- - **AI-Native Foundation**: Built-in `.cursorrules` optimized for **Cursor** & AI agents—projects are "Born to be Autonomously Coded."
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** support (Refresh/Access tokens).
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
- ## 5,280+ Project Combinations
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
- - **480 Core Combinations**:
98
- - **MVC Architecture**: 360 variants (Languages × View Engines × Databases × Communication Patterns × Caching × Auth)
99
- - **Clean Architecture**: 120 variants (Languages × Databases × Communication Patterns × Caching × Auth)
100
- - **5,280+ Total Scenarios**:
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:
@@ -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. MVC Views (if applicable)
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
- // 6. Restructuring for Clean Architecture
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: 'auth',
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
- // Normalize auth to array if it's a string from the list prompt
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 = [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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-quickstart-structure",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
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",
@@ -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);
@@ -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();
@@ -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')) { -%>app.use('/api/auth', authRoutes);<%_ } -%>
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
 
@@ -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