nodejs-quickstart-structure 2.1.2 → 2.2.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 (66) hide show
  1. package/CHANGELOG.md +11 -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 +44 -4
  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 +3 -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/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
  16. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
  17. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  18. package/templates/common/.env.example.ejs +10 -0
  19. package/templates/common/README.md.ejs +65 -14
  20. package/templates/common/auth/js/controllers/authController.js.ejs +326 -13
  21. package/templates/common/auth/js/controllers/authController.spec.js.ejs +237 -51
  22. package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
  23. package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
  24. package/templates/common/auth/js/services/jwtService.js.ejs +3 -3
  25. package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
  26. package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
  27. package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +194 -0
  28. package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
  29. package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
  30. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +344 -64
  31. package/templates/common/auth/ts/controllers/authController.ts.ejs +341 -9
  32. package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
  33. package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
  34. package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
  35. package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
  36. package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
  37. package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
  38. package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
  39. package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
  40. package/templates/common/database/js/models/User.js.ejs +13 -5
  41. package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
  42. package/templates/common/database/ts/models/User.ts.ejs +23 -7
  43. package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
  44. package/templates/common/docker-compose.yml.ejs +21 -0
  45. package/templates/common/ecosystem.config.js.ejs +10 -0
  46. package/templates/common/jest.config.js.ejs +1 -1
  47. package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
  48. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
  49. package/templates/common/package.json.ejs +2 -0
  50. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
  51. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
  52. package/templates/common/swagger.yml.ejs +62 -3
  53. package/templates/common/views/ejs/login.ejs.ejs +84 -0
  54. package/templates/common/views/ejs/signup.ejs.ejs +84 -0
  55. package/templates/common/views/pug/login.pug.ejs +78 -0
  56. package/templates/common/views/pug/signup.pug.ejs +78 -0
  57. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
  58. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
  59. package/templates/mvc/js/src/config/env.js.ejs +12 -2
  60. package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
  61. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
  62. package/templates/mvc/ts/src/config/env.ts.ejs +12 -2
  63. package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
  64. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  65. package/templates/mvc/ts/src/index.ts.ejs +2 -1
  66. package/templates/clean-architecture/ts/src/domain/user.ts +0 -9
@@ -45,4 +45,14 @@ JWT_SECRET=your_jwt_secret_key_here_change_it
45
45
  JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here_change_it
46
46
  JWT_EXPIRES_IN=15m
47
47
  JWT_REFRESH_EXPIRES_IN=7d
48
+ <%_ if (socialAuth && socialAuth.includes('Google')) { -%>
49
+ GOOGLE_CLIENT_ID=your_google_client_id
50
+ GOOGLE_CLIENT_SECRET=your_google_client_secret
51
+ GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback
52
+ <%_ } -%>
53
+ <%_ if (socialAuth && socialAuth.includes('GitHub')) { -%>
54
+ GITHUB_CLIENT_ID=your_github_client_id
55
+ GITHUB_CLIENT_SECRET=your_github_client_secret
56
+ GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback
57
+ <%_ } -%>
48
58
  <% } -%>
@@ -13,7 +13,7 @@ This project follows a strict **7-Step Production-Ready Process** to ensure qual
13
13
 
14
14
  ---
15
15
 
16
- ## 🚀 7-Step Production-Ready Process
16
+ ## 7-Step Production-Ready Process
17
17
 
18
18
  1. **Initialize Git**: `git init` (Required for Husky hooks and security gates).
19
19
  2. **Install Dependencies**: `npm install`.
@@ -25,7 +25,7 @@ This project follows a strict **7-Step Production-Ready Process** to ensure qual
25
25
 
26
26
  ---
27
27
 
28
- ## 🚀 Key Features
28
+ ## Key Features
29
29
 
30
30
  - **Architecture**: <%= architecture %> (<% if (architecture === 'Clean Architecture') { %>Domain, UseCases, Infrastructure<% } else { %>MVC Pattern<% } %>).
31
31
  - **Database**: <%= database %> <% if (database !== 'None') { %>(via <%= database === 'MongoDB' ? 'Mongoose' : 'Sequelize' %>)<% } %>.
@@ -157,20 +157,71 @@ API is exposed via **REST**.
157
157
  A Swagger UI for API documentation is available at:
158
158
  - **URL**: `http://localhost:3000/api-docs` (Dynamic based on PORT)
159
159
 
160
- ### 🛣️ User Endpoints:
160
+ ### User Endpoints:
161
161
  - `GET /api/users`: List all users.
162
162
  - `GET /api/users/:id`: Get a user by ID.
163
163
  - `POST /api/users`: Create a new user.
164
164
  - `PATCH /api/users/:id`: Partially update a user.
165
165
  - `DELETE /api/users/:id`: Delete a user (Soft Delete).
166
166
  <%_ if (auth.includes('JWT')) { %>
167
- ### 🔐 Auth Endpoints:
167
+ ### Auth Endpoints:
168
168
  - `POST /api/auth/login`: Exchange credentials for a short-lived `accessToken` and a long-lived `refreshToken`.
169
169
  - `POST /api/auth/refresh`: Submit a `refreshToken` to receive a new pair of tokens. (Includes theft-detection logic).
170
170
  - `POST /api/auth/logout`: Revoke (blacklist) the active `accessToken` and delete the `refreshToken`.
171
171
  - `POST /api/users`: Acts as Sign Up when password is provided.
172
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
173
+ ### Social Authentication Flows
174
+
175
+ This project supports two distinct social authentication flows:
176
+
177
+ #### 1. The Redirection Flow (Best for MVC/Web)
178
+ Standard OAuth2 flow using browser redirects.
179
+ - **Start**: `GET /api/auth/google` (or `/github`)
180
+ - **Callback**: Handled automatically by the backend via `/google/callback`.
181
+ - **Result**: User is logged in and redirected to home; `accessToken` and `refreshToken` are securely saved as **HttpOnly cookies** in the browser.
182
+ - **Callback URL**: `http://localhost:3000/api/auth/google/callback` (Standardized for both MVC and Clean Architecture).
183
+
184
+ > [!TIP]
185
+ > **Testing in Swagger**: The "Execute" button in Swagger UI will show a "Failed to fetch" error for this route because browsers block redirects to external domains (like Google) inside AJAX requests. To test this, simply open `http://localhost:3000/api/auth/google` directly in your browser tab.
186
+
187
+ #### 2. The Exchange Flow (Best for SPAs/Mobile Apps)
188
+ A headless flow where the client provides the OAuth code.
189
+ - **API**: `POST /api/auth/social/exchange`
190
+ - **Body**: `{ "code": "AUTH_CODE", "provider": "Google" }`
191
+ - **Result**: Returns JWT tokens (`accessToken`, `refreshToken`) in the JSON response.
192
+
193
+ ---
194
+ <%_ } _%>
172
195
  *Note: To access protected user endpoints (GET/PATCH/DELETE), include `Authorization: Bearer <your_accessToken>` in the headers.*
173
196
  <% } -%>
197
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
198
+ ### Social Authentication Setup
199
+ To use social login, you must configure the following in your `.env`:
200
+
201
+ #### Callback URL Configuration
202
+ For the best practice standardized API structure, you **must** configure the Redirect URIs in your developer portals (Google/GitHub) as follows:
203
+
204
+ | Provider | Redirect URI (Callback URL) |
205
+ | :--- | :--- |
206
+ | **Google** | `http://localhost:3000/api/auth/google/callback` |
207
+ | **GitHub** | `http://localhost:3000/api/auth/github/callback` |
208
+
209
+ > [!IMPORTANT]
210
+ > This standardized path works for both **MVC** and **Clean Architecture** templates.
211
+
212
+ #### <% if (socialAuth.includes('Google')) { %>1. Google Integration<% } %>
213
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/).
214
+ 2. Create/Select a project and go to **APIs & Services > Credentials**.
215
+ 3. Create an **OAuth client ID** for a **Web application**.
216
+ 4. Add Redirect URI: `http://localhost:3000/api/auth/google/callback`.
217
+ 5. Set `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and `GOOGLE_CALLBACK_URL` in `.env`.
218
+
219
+ #### <% if (socialAuth.includes('GitHub')) { %>2. GitHub Integration<% } %>
220
+ 1. Go to [GitHub Developer Settings](https://github.com/settings/developers).
221
+ 2. Register a **New OAuth App**.
222
+ 3. Set Callback URL: `http://localhost:3000/api/auth/github/callback`.
223
+ 4. Set `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, and `GITHUB_CALLBACK_URL` in `.env`.
224
+ <% } -%>
174
225
  <% } -%>
175
226
  <% if (communication === 'Kafka') { -%>
176
227
  ## 📡 Testing Kafka Asynchronous Flow
@@ -212,10 +263,10 @@ curl -X PATCH http://localhost:3000/api/users/1 \
212
263
  ```text
213
264
  [Kafka] Producer: Sent USER_CREATED event for 'kafka@example.com'
214
265
  [Kafka] Consumer: Received USER_CREATED.
215
- [Kafka] Consumer: 📧 Sending welcome email to 'kafka@example.com'... Done!
266
+ [Kafka] Consumer: Sending welcome email to 'kafka@example.com'... Done!
216
267
  ```
217
268
 
218
- ### 🛠️ Kafka Troubleshooting
269
+ ### Kafka Troubleshooting
219
270
  If the connection or events are failing:
220
271
  1. **Check Docker**: Ensure Kafka container is running (`docker ps`).
221
272
  2. **Verify Broker**: `KAFKA_BROKER` in `.env` must match your host/port (standard: 9093).
@@ -224,24 +275,24 @@ If the connection or events are failing:
224
275
  <% } -%>
225
276
 
226
277
  <% if (caching === 'Redis') { -%>
227
- ## Caching
278
+ ## Caching
228
279
  This project uses **Redis** for caching.
229
280
  - **Client**: `ioredis`
230
281
  - **Connection**: Configured via `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD` in `.env`.
231
282
  <% } else if (caching === 'Memory Cache') { -%>
232
- ## Caching
283
+ ## Caching
233
284
  This project uses **Memory Cache** for in-memory caching.
234
285
  - **Client**: `node-cache`
235
286
  <% } -%>
236
287
 
237
- ## 📝 Logging
288
+ ## Logging
238
289
  This project uses **Winston** for structured logging.
239
290
  - **Development**: Logs are printed to the console.
240
291
  - **Production**: Logs are saved to files:
241
292
  - `error.log`: Only error level logs.
242
293
  - `combined.log`: All logs.
243
294
 
244
- ## 🐳 Docker Deployment
295
+ ## Docker Deployment
245
296
  This project uses a **Multi-Stage Dockerfile** for optimized production images.
246
297
 
247
298
  <% if (database !== 'None' || caching === 'Redis' || communication === 'Kafka') { -%>
@@ -288,7 +339,7 @@ docker build -t <%= projectName %> .
288
339
  docker run -p 3000:3000 <%= projectName %>
289
340
  ```
290
341
  <% } -%>
291
- ## 🚀 PM2 Deployment (VPS/EC2)
342
+ ## PM2 Deployment (VPS/EC2)
292
343
  This project is pre-configured for direct deployment to a VPS/EC2 instance using **PM2** (via `ecosystem.config.js`).
293
344
  1. Install dependencies
294
345
  ```bash
@@ -324,17 +375,17 @@ docker-compose down
324
375
  - **Rate Limiting**: Protects against DDoS / Brute-force.
325
376
  - **HPP**: Prevents HTTP Parameter Pollution attacks.
326
377
  <% if (includeSecurity) { %>
327
- ### 🛡️ Enterprise Hardening (Big Tech Standard)
378
+ ### Enterprise Hardening (Big Tech Standard)
328
379
  - **Snyk SCA**: Run `npm run snyk:test` for dependency scanning.
329
380
  - **SonarCloud**: Automated SAST on every Push/PR.
330
381
  - **Digital Guardians**: Recommended Gitleaks integration for secret protection.
331
382
  - **Security Policy**: Standard `SECURITY.md` for vulnerability reporting.
332
383
  <% } %>
333
- ## 🤖 AI-Native Development
384
+ ## AI-Native Development
334
385
 
335
386
  This project is "AI-Ready" out of the box. We have pre-configured industry-leading AI context files to bridge the gap between "Generated Code" and "AI-Assisted Development."
336
387
 
337
388
  - **Magic Defaults**: We've automatically tailored your AI context to focus on **<%= projectName %>** and its specific architectural stack (<%= architecture %>, <%= database %>, etc.).
338
389
  - **Use Cursor?** We've configured **`.cursorrules`** at the root. It enforces project standards (80% coverage, MVC/Clean) directly within the editor.
339
- - *Pro-tip*: You can customize the `Project Goal` placeholder in `.cursorrules` to help the AI understand your specific business logic!
390
+ - *Pro-tip*: You can customize the `Project Goal` placeholder in `.cursorrules` to help the AI understand your specific business logic!
340
391
  - **Use ChatGPT/Gemini/Claude?** Check the **`prompts/`** directory. It contains highly-specialized Agent Skill templates. You can copy-paste these into any LLM to give it a "Senior Developer" understanding of your codebase immediately.
@@ -1,19 +1,27 @@
1
1
  const bcrypt = require('bcryptjs');
2
- <% if (architecture === 'MVC') { -%>
2
+ <%_ if (architecture === 'MVC') { _%>
3
3
  const User = require('../models/User');
4
4
  const JwtService = require('../services/jwtService');
5
- <% if (caching !== 'None') { -%>
5
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
6
+ const { SocialAuthService } = require('../services/socialAuthService');
7
+ <%_ } _%>
8
+ <%_ if (caching !== 'None') { _%>
6
9
  const cacheService = require('<% if (caching === "Redis") { %>../config/redisClient<% } else { %>../config/memoryCache<% } %>');
7
- <% } -%>
10
+ <%_ } _%>
8
11
  const logger = require('../utils/logger');
9
- <% } else { -%>
12
+ <%_ } else { _%>
10
13
  const User = require('../../../infrastructure/database/models/User');
11
14
  const JwtService = require('../../../infrastructure/auth/jwtService');
12
- <% if (caching !== 'None') { -%>
15
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
16
+ const { SocialLoginUseCase } = require('../../../usecases/auth/socialLoginUseCase');
17
+ const { GoogleProvider, GitHubProvider } = require('../../../infrastructure/auth/socialAuthService');
18
+ const UserRepository = require('../../../infrastructure/repositories/UserRepository');
19
+ <%_ } _%>
20
+ <%_ if (caching !== 'None') { _%>
13
21
  const cacheService = require('<% if (caching === "Redis") { %>../../../infrastructure/caching/redisClient<% } else { %>../../../infrastructure/caching/memoryCache<% } %>');
14
- <% } -%>
22
+ <%_ } _%>
15
23
  const logger = require('../../../infrastructure/log/logger');
16
- <% } -%>
24
+ <%_ } _%>
17
25
  const HTTP_STATUS = require('<% if (architecture === "MVC") { %>../utils/httpCodes<% } else { %>../../../utils/httpCodes<% } %>');
18
26
 
19
27
  class AuthController {
@@ -21,6 +29,17 @@ class AuthController {
21
29
  this.login = this.login.bind(this);
22
30
  this.refresh = this.refresh.bind(this);
23
31
  this.logout = this.logout.bind(this);
32
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
33
+ this.socialExchange = this.socialExchange.bind(this);
34
+ <% if (socialAuth.includes('Google')) { -%>
35
+ this.googleLogin = this.googleLogin.bind(this);
36
+ this.googleCallback = this.googleCallback.bind(this);
37
+ <% } -%>
38
+ <% if (socialAuth.includes('GitHub')) { -%>
39
+ this.githubLogin = this.githubLogin.bind(this);
40
+ this.githubCallback = this.githubCallback.bind(this);
41
+ <% } -%>
42
+ <% } -%>
24
43
  }
25
44
 
26
45
  async login(req, res, next) {
@@ -45,16 +64,16 @@ class AuthController {
45
64
  const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
46
65
  const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
47
66
 
48
- <%_ if (caching !== 'None') { -%>
67
+ <%_ if (caching !== 'None') { _%>
49
68
  const cacheKey = `refresh_tokens:${userId}`;
50
69
  const activeTokens = await cacheService.get(cacheKey) || [];
51
70
  activeTokens.push(refreshJti);
52
71
  await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
53
- <% } else { %>
72
+ <%_ } else { _%>
54
73
  const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
55
74
  activeTokens.push(refreshJti);
56
75
  JwtService.activeRefreshTokens.set(userId, activeTokens);
57
- <%_ } -%>
76
+ <%_ } _%>
58
77
 
59
78
  res.json({ token: accessToken, accessToken, refreshToken });
60
79
  } catch (error) {
@@ -78,7 +97,7 @@ class AuthController {
78
97
  const userId = String(decoded.id);
79
98
  const incomingJti = decoded.jti;
80
99
 
81
- <%_ if (caching !== 'None') { -%>
100
+ <%_ if (caching !== 'None') { _%>
82
101
  const cacheKey = `refresh_tokens:${userId}`;
83
102
  let activeTokens = await cacheService.get(cacheKey) || [];
84
103
 
@@ -95,7 +114,7 @@ class AuthController {
95
114
 
96
115
  activeTokens.push(newRefreshJti);
97
116
  await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
98
- <% } else { %>
117
+ <%_ } else { _%>
99
118
  let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
100
119
 
101
120
  if (!activeTokens.includes(incomingJti)) {
@@ -111,7 +130,7 @@ class AuthController {
111
130
 
112
131
  activeTokens.push(newRefreshJti);
113
132
  JwtService.activeRefreshTokens.set(userId, activeTokens);
114
- <%_ } -%>
133
+ <%_ } _%>
115
134
  res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
116
135
  } catch (error) {
117
136
  logger.error('Refresh token error:', error);
@@ -165,6 +184,300 @@ class AuthController {
165
184
  next(error);
166
185
  }
167
186
  }
187
+
188
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
189
+ async socialExchange(req, res, next) {
190
+ try {
191
+ const { code, provider, redirectUri } = req.body;
192
+ if (!code || !provider) {
193
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Code and provider are required' });
194
+ }
195
+
196
+ <% if (architecture === 'Clean Architecture') { -%>
197
+ let useCase;
198
+ const userRepository = new UserRepository();
199
+ <% if (socialAuth.includes('Google')) { -%>
200
+ if (provider === 'Google') useCase = new SocialLoginUseCase(new GoogleProvider(), userRepository);
201
+ <% } -%>
202
+ <% if (socialAuth.includes('GitHub')) { -%>
203
+ if (provider === 'GitHub') useCase = new SocialLoginUseCase(new GitHubProvider(), userRepository);
204
+ <% } -%>
205
+
206
+ if (!useCase) {
207
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Invalid social provider' });
208
+ }
209
+
210
+ const { user, accessToken, refreshToken } = await useCase.execute(code, redirectUri);
211
+ const userId = String(user.id || user._id);
212
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
213
+
214
+ // Store refresh token
215
+ <% if (caching !== 'None') { -%>
216
+ const cacheKey = `refresh_tokens:${userId}`;
217
+ const activeTokens = await cacheService.get(cacheKey) || [];
218
+ activeTokens.push(refreshJti);
219
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
220
+ <% } else { -%>
221
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
222
+ activeTokens.push(refreshJti);
223
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
224
+ <% } -%>
225
+
226
+ res.json({ token: accessToken, accessToken, refreshToken });
227
+ <% } else { -%>
228
+ let profile;
229
+ <% if (socialAuth.includes('Google')) { -%>
230
+ if (provider === 'Google') {
231
+ profile = await SocialAuthService.getGoogleProfile(code, redirectUri);
232
+ }
233
+ <% } -%>
234
+ <% if (socialAuth.includes('GitHub')) { -%>
235
+ if (provider === 'GitHub') {
236
+ profile = await SocialAuthService.getGithubProfile(code);
237
+ }
238
+ <% } -%>
239
+
240
+ if (!profile) {
241
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ message: 'Invalid social provider' });
242
+ }
243
+
244
+ if (!profile || !profile.email) {
245
+ return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Failed to retrieve profile from provider' });
246
+ }
247
+
248
+ <%_ if (database === 'MongoDB' || database === 'None') { _%>
249
+ let user = await User.findOne({ email: profile.email });
250
+ <%_ } else { _%>
251
+ let user = await User.findOne({ where: { email: profile.email } });
252
+ <%_ } _%>
253
+
254
+ if (!user) {
255
+ // Create new user without password
256
+ <%_ if (database === 'MongoDB' || database === 'None') { _%>
257
+ user = await User.create({ email: profile.email, name: profile.name, password: null });
258
+ <%_ if (socialAuth.includes('Google')) { _%>
259
+ if (provider === 'Google') user.googleId = profile.id;
260
+ <%_ } _%>
261
+ <%_ if (socialAuth.includes('GitHub')) { _%>
262
+ if (provider === 'GitHub') user.githubId = profile.id;
263
+ <%_ } _%>
264
+ await user.save();
265
+ <%_ } else { _%>
266
+ user = await User.create({
267
+ email: profile.email,
268
+ name: profile.name,
269
+ password: null,
270
+ <%_ if (socialAuth.includes('Google')) { _%>
271
+ googleId: provider === 'Google' ? profile.id : null,
272
+ <%_ } _%>
273
+ <%_ if (socialAuth.includes('GitHub')) { _%>
274
+ githubId: provider === 'GitHub' ? profile.id : null,
275
+ <%_ } _%>
276
+ });
277
+ <%_ } _%>
278
+ }
279
+
280
+ const userId = String(user.id || user._id);
281
+
282
+ const refreshToken = JwtService.generateRefreshToken({ id: userId, email: user.email });
283
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
284
+ const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
285
+
286
+ // Store refresh token
287
+ <%_ if (caching !== 'None') { -%>
288
+ const cacheKey = `refresh_tokens:${userId}`;
289
+ const activeTokens = await cacheService.get(cacheKey) || [];
290
+ activeTokens.push(refreshJti);
291
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
292
+ <%_ } else { -%>
293
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
294
+ activeTokens.push(refreshJti);
295
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
296
+ <%_ } _%>
297
+
298
+ res.json({ token: accessToken, accessToken, refreshToken });
299
+ <%_ } _%>
300
+ } catch (error) {
301
+ logger.error('Social exchange error:', error);
302
+ next(error);
303
+ }
304
+ }
305
+ <% } -%>
306
+
307
+ <% if (socialAuth.includes('Google')) { -%>
308
+ async googleLogin(req, res) {
309
+ const rootUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
310
+ const options = {
311
+ redirect_uri: process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/api/auth/google/callback',
312
+ client_id: process.env.GOOGLE_CLIENT_ID,
313
+ access_type: 'offline',
314
+ response_type: 'code',
315
+ prompt: 'consent',
316
+ scope: [
317
+ 'https://www.googleapis.com/auth/userinfo.profile',
318
+ 'https://www.googleapis.com/auth/userinfo.email',
319
+ ].join(' '),
320
+ state: 'google'
321
+ };
322
+ const qs = new URLSearchParams(options);
323
+ res.redirect(`${rootUrl}?${qs.toString()}`);
324
+ }
325
+
326
+ async googleCallback(req, res, next) {
327
+ try {
328
+ const { code } = req.query;
329
+ const redirectUri = process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/api/auth/google/callback';
330
+
331
+ <%_ if (architecture === 'Clean Architecture') { _%>
332
+ const useCase = new SocialLoginUseCase(new GoogleProvider(), new UserRepository());
333
+ const { user, accessToken, refreshToken } = await useCase.execute(code, redirectUri);
334
+ const userId = String(user.id || user._id);
335
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
336
+
337
+ // Store refresh token
338
+ <%_ if (caching !== 'None') { _%>
339
+ const cacheKey = `refresh_tokens:${userId}`;
340
+ const activeTokens = await cacheService.get(cacheKey) || [];
341
+ activeTokens.push(refreshJti);
342
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
343
+ <%_ } else { _%>
344
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
345
+ activeTokens.push(refreshJti);
346
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
347
+ <%_ } _%>
348
+
349
+ res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
350
+ res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
351
+ res.redirect('/');
352
+ <%_ } else { _%>
353
+ const profile = await SocialAuthService.getGoogleProfile(code, redirectUri);
354
+
355
+ <%_ if (database === 'MongoDB' || database === 'None') { -%>
356
+ let user = await User.findOne({ email: profile.email });
357
+ <%_ } else { -%>
358
+ let user = await User.findOne({ where: { email: profile.email } });
359
+ <%_ } -%>
360
+
361
+ if (!user) {
362
+ user = await User.create({
363
+ email: profile.email,
364
+ name: profile.name,
365
+ password: null,
366
+ googleId: profile.id
367
+ });
368
+ }
369
+
370
+ const userId = String(user.id || user._id);
371
+ const refreshToken = JwtService.generateRefreshToken({ id: userId, email: user.email });
372
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
373
+ const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
374
+
375
+ // Store refresh token
376
+ <%_ if (caching !== 'None') { -%>
377
+ const cacheKey = `refresh_tokens:${userId}`;
378
+ const activeTokens = await cacheService.get(cacheKey) || [];
379
+ activeTokens.push(refreshJti);
380
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
381
+ <%_ } else { _%>
382
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
383
+ activeTokens.push(refreshJti);
384
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
385
+ <%_ } _%>
386
+
387
+ res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
388
+ res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
389
+ res.redirect('/');
390
+ <%_ } _%>
391
+ } catch (error) {
392
+ logger.error('Google callback error:', error);
393
+ res.redirect('/login?error=social_auth_failed');
394
+ }
395
+ }
396
+ <% } -%>
397
+
398
+ <% if (socialAuth.includes('GitHub')) { -%>
399
+ async githubLogin(req, res) {
400
+ const rootUrl = 'https://github.com/login/oauth/authorize';
401
+ const options = {
402
+ client_id: process.env.GITHUB_CLIENT_ID,
403
+ redirect_uri: process.env.GITHUB_CALLBACK_URL || 'http://localhost:3000/api/auth/github/callback',
404
+ scope: 'user:email',
405
+ state: 'github'
406
+ };
407
+ const qs = new URLSearchParams(options);
408
+ res.redirect(`${rootUrl}?${qs.toString()}`);
409
+ }
410
+
411
+ async githubCallback(req, res, next) {
412
+ try {
413
+ const { code } = req.query;
414
+
415
+ <%_ if (architecture === 'Clean Architecture') { _%>
416
+ const useCase = new SocialLoginUseCase(new GitHubProvider(), new UserRepository());
417
+ const { user, accessToken, refreshToken } = await useCase.execute(code);
418
+ const userId = String(user.id || user._id);
419
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
420
+
421
+ // Store refresh token
422
+ <%_ if (caching !== 'None') { _%>
423
+ const cacheKey = `refresh_tokens:${userId}`;
424
+ const activeTokens = await cacheService.get(cacheKey) || [];
425
+ activeTokens.push(refreshJti);
426
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
427
+ <%_ } else { _%>
428
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
429
+ activeTokens.push(refreshJti);
430
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
431
+ <%_ } _%>
432
+
433
+ res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
434
+ res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
435
+ res.redirect('/');
436
+ <%_ } else { _%>
437
+ const profile = await SocialAuthService.getGithubProfile(code);
438
+
439
+ <%_ if (database === 'MongoDB' || database === 'None') { -%>
440
+ let user = await User.findOne({ email: profile.email });
441
+ <%_ } else { -%>
442
+ let user = await User.findOne({ where: { email: profile.email } });
443
+ <%_ } -%>
444
+
445
+ if (!user) {
446
+ user = await User.create({
447
+ email: profile.email,
448
+ name: profile.name,
449
+ password: null,
450
+ githubId: profile.id
451
+ });
452
+ }
453
+
454
+ const userId = String(user.id || user._id);
455
+ const refreshToken = JwtService.generateRefreshToken({ id: userId, email: user.email });
456
+ const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
457
+ const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
458
+
459
+ // Store refresh token
460
+ <%_ if (caching !== 'None') { -%>
461
+ const cacheKey = `refresh_tokens:${userId}`;
462
+ const activeTokens = await cacheService.get(cacheKey) || [];
463
+ activeTokens.push(refreshJti);
464
+ await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
465
+ <%_ } else { -%>
466
+ const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
467
+ activeTokens.push(refreshJti);
468
+ JwtService.activeRefreshTokens.set(userId, activeTokens);
469
+ <%_ } _%>
470
+
471
+ res.cookie('accessToken', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
472
+ res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' });
473
+ res.redirect('/');
474
+ <%_ } _%>
475
+ } catch (error) {
476
+ logger.error('GitHub callback error:', error);
477
+ res.redirect('/login?error=social_auth_failed');
478
+ }
479
+ }
480
+ <% } -%>
168
481
  }
169
482
 
170
483
  module.exports = AuthController;