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.
- package/CHANGELOG.md +11 -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 +44 -4
- 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 +3 -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/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/common/.env.example.ejs +10 -0
- package/templates/common/README.md.ejs +65 -14
- package/templates/common/auth/js/controllers/authController.js.ejs +326 -13
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +237 -51
- 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.js.ejs +3 -3
- 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 +194 -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 +344 -64
- package/templates/common/auth/ts/controllers/authController.ts.ejs +341 -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/jest.config.js.ejs +1 -1
- package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
- package/templates/common/package.json.ejs +2 -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/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 +2 -1
- 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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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:
|
|
266
|
+
[Kafka] Consumer: Sending welcome email to 'kafka@example.com'... Done!
|
|
216
267
|
```
|
|
217
268
|
|
|
218
|
-
###
|
|
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
|
-
##
|
|
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
|
-
##
|
|
283
|
+
## Caching
|
|
233
284
|
This project uses **Memory Cache** for in-memory caching.
|
|
234
285
|
- **Client**: `node-cache`
|
|
235
286
|
<% } -%>
|
|
236
287
|
|
|
237
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
###
|
|
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
|
-
##
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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;
|