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
@@ -4,7 +4,13 @@ export interface User {
4
4
  name: string;
5
5
  email: string;
6
6
  <% if (auth.includes('JWT')) { -%>
7
- password?: string;
7
+ password?: string | null;
8
+ <% } -%>
9
+ <% if (socialAuth && socialAuth.includes('Google')) { -%>
10
+ googleId?: string | null;
11
+ <% } -%>
12
+ <% if (socialAuth && socialAuth.includes('GitHub')) { -%>
13
+ githubId?: string | null;
8
14
  <% } -%>
9
15
  }
10
16
 
@@ -61,7 +67,9 @@ class User extends Model {
61
67
  public id!: number;
62
68
  public name!: string;
63
69
  public email!: string;
64
- <% if (auth.includes('JWT')) { %>public password!: string;<% } %>
70
+ <% if (auth.includes('JWT')) { %>public password?: string | null;<% } %>
71
+ <% if (socialAuth && socialAuth.includes('Google')) { %>public googleId?: string | null;<% } %>
72
+ <% if (socialAuth && socialAuth.includes('GitHub')) { %>public githubId?: string | null;<% } %>
65
73
  }
66
74
 
67
75
  User.init(
@@ -79,13 +87,21 @@ User.init(
79
87
  type: DataTypes.STRING,
80
88
  allowNull: false,
81
89
  unique: true,
82
- },
83
- <% if (auth.includes('JWT')) { %>
90
+ },<% if (auth.includes('JWT')) { %>
84
91
  password: {
85
92
  type: DataTypes.STRING,
86
- allowNull: false,
87
- },
88
- <% } %>
93
+ allowNull: true,
94
+ },<% } %><% if (socialAuth && socialAuth.includes('Google')) { %>
95
+ googleId: {
96
+ type: DataTypes.STRING,
97
+ allowNull: true,
98
+ unique: true,
99
+ },<% } %><% if (socialAuth && socialAuth.includes('GitHub')) { %>
100
+ githubId: {
101
+ type: DataTypes.STRING,
102
+ allowNull: true,
103
+ unique: true,
104
+ },<% } %>
89
105
  deletedAt: {
90
106
  type: DataTypes.DATE,
91
107
  allowNull: true,
@@ -3,7 +3,9 @@ import mongoose, { Schema, Document } from 'mongoose';
3
3
  export interface IUser extends Document {
4
4
  name: string;
5
5
  email: string;
6
- <% if (auth.includes('JWT')) { %>password: string;<% } %>
6
+ <% if (auth.includes('JWT')) { %>password?: string | null;<% } %>
7
+ <% if (socialAuth && socialAuth.includes('Google')) { %>googleId?: string | null;<% } %>
8
+ <% if (socialAuth && socialAuth.includes('GitHub')) { %>githubId?: string | null;<% } %>
7
9
  createdAt: Date;
8
10
  deletedAt?: Date | null;
9
11
  }
@@ -21,7 +23,21 @@ const UserSchema: Schema = new Schema({
21
23
  <% if (auth.includes('JWT')) { %>
22
24
  password: {
23
25
  type: String,
24
- required: true
26
+ required: false
27
+ },
28
+ <% } %>
29
+ <% if (socialAuth && socialAuth.includes('Google')) { %>
30
+ googleId: {
31
+ type: String,
32
+ unique: true,
33
+ sparse: true
34
+ },
35
+ <% } %>
36
+ <% if (socialAuth && socialAuth.includes('GitHub')) { %>
37
+ githubId: {
38
+ type: String,
39
+ unique: true,
40
+ sparse: true
25
41
  },
26
42
  <% } %>
27
43
  createdAt: {
@@ -44,6 +44,16 @@ services:
44
44
  - JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here_change_it
45
45
  - JWT_EXPIRES_IN=1d
46
46
  - JWT_REFRESH_EXPIRES_IN=7d
47
+ <%_ if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('Google')) { -%>
48
+ - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-your_google_id}
49
+ - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-your_google_secret}
50
+ - GOOGLE_CALLBACK_URL=${GOOGLE_CALLBACK_URL:-http://localhost:3000/api/auth/google/callback}
51
+ <%_ } -%>
52
+ <%_ if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('GitHub')) { -%>
53
+ - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-your_github_id}
54
+ - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-your_github_secret}
55
+ - GITHUB_CALLBACK_URL=${GITHUB_CALLBACK_URL:-http://localhost:3000/api/auth/github/callback}
56
+ <%_ } -%>
47
57
  <%_ } -%>
48
58
  <%_ } else { -%>
49
59
  environment:
@@ -75,8 +85,19 @@ services:
75
85
  - JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here_change_it
76
86
  - JWT_EXPIRES_IN=1d
77
87
  - JWT_REFRESH_EXPIRES_IN=7d
88
+ <%_ if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('Google')) { -%>
89
+ - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-your_google_id}
90
+ - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-your_google_secret}
91
+ - GOOGLE_CALLBACK_URL=${GOOGLE_CALLBACK_URL:-http://localhost:3000/api/auth/google/callback}
92
+ <%_ } -%>
93
+ <%_ if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('GitHub')) { -%>
94
+ - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-your_github_id}
95
+ - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-your_github_secret}
96
+ - GITHUB_CALLBACK_URL=${GITHUB_CALLBACK_URL:-http://localhost:3000/api/auth/github/callback}
78
97
  <%_ } -%>
79
98
  <%_ } -%>
99
+ <%_ } -%>
100
+
80
101
  <%_ if (database !== 'None') { -%>
81
102
  db:
82
103
  <%_ if (database === 'MySQL') { -%>
@@ -40,6 +40,16 @@ module.exports = {
40
40
  JWT_REFRESH_SECRET: "your_jwt_refresh_secret_here",
41
41
  JWT_EXPIRES_IN: "1h",
42
42
  JWT_REFRESH_EXPIRES_IN: "7d",
43
+ <%_ if (socialAuth && socialAuth.includes('Google')) { -%>
44
+ GOOGLE_CLIENT_ID: "your_google_client_id",
45
+ GOOGLE_CLIENT_SECRET: "your_google_client_secret",
46
+ GOOGLE_CALLBACK_URL: "http://localhost:3000/api/auth/google/callback",
47
+ <%_ } -%>
48
+ <%_ if (socialAuth && socialAuth.includes('GitHub')) { -%>
49
+ GITHUB_CLIENT_ID: "your_github_client_id",
50
+ GITHUB_CLIENT_SECRET: "your_github_client_secret",
51
+ GITHUB_CALLBACK_URL: "http://localhost:3000/api/auth/github/callback",
52
+ <%_ } -%>
43
53
  <%_ } -%>
44
54
  }
45
55
  }]
@@ -73,7 +73,10 @@ export default [
73
73
  },
74
74
  rules: {
75
75
  "no-console": "warn",
76
- "no-unused-vars": "warn",
76
+ "no-unused-vars": ["warn", {
77
+ "argsIgnorePattern": "^_",
78
+ "varsIgnorePattern": "^_"
79
+ }],
77
80
  "import/no-unresolved": [2, { "caseSensitive": false }]
78
81
  }
79
82
  }
@@ -4,7 +4,7 @@ module.exports = {
4
4
  coverageDirectory: 'coverage',
5
5
  collectCoverageFrom: ['src/**/*.{js,ts}'],
6
6
  testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.spec.ts', '**/*.spec.js'],
7
- transformIgnorePatterns: ['/node_modules/(?!.*uuid)'],
7
+ transformIgnorePatterns: ['/node_modules/(?!axios|sequelize|@sequelize|uuid)'],
8
8
  testPathIgnorePatterns: ['/node_modules/', '/tests/e2e/'],
9
9
  <% if (language === 'TypeScript') { %>preset: 'ts-jest',<% } %>
10
10
  moduleNameMapper: {
@@ -81,7 +81,7 @@ export class KafkaService {
81
81
  try {
82
82
  const parsed = JSON.parse(message);
83
83
  logger.info(`[Kafka] Producer: Sent ${parsed.action} event for '${parsed.payload?.email || 'unknown'}'`);
84
- } catch {
84
+ } catch (error) {
85
85
  logger.info(`[Kafka] Producer: Sent message to ${topic}`);
86
86
  }
87
87
  }
@@ -57,9 +57,12 @@
57
57
  <% } -%>
58
58
  <% if (auth.includes('JWT')) { %> "jsonwebtoken": "^9.0.2",
59
59
  "bcryptjs": "^2.4.3",
60
+ <% } -%>
61
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { %> "axios": "^1.6.5",
60
62
  <% } -%>
61
63
  "cors": "^2.8.5",
62
64
  "helmet": "^7.1.0",
65
+ "cookie-parser": "^1.4.6",
63
66
  "hpp": "^0.2.3",
64
67
  "express-rate-limit": "^7.1.5",
65
68
  "winston": "^3.11.0",
@@ -78,6 +81,7 @@
78
81
  "ts-node": "^10.9.2",
79
82
  "@types/node": "^20.10.5",
80
83
  "@types/express": "^4.17.21",
84
+ "@types/cookie-parser": "^1.4.6",
81
85
  "@types/cors": "^2.8.17",
82
86
  "@types/hpp": "^0.2.3",
83
87
  <% if (caching === 'Memory Cache') { %> "@types/node-cache": "^4.2.5",
@@ -62,15 +62,27 @@ describe('E2E User Tests', () => {
62
62
  <%_ } -%>
63
63
 
64
64
  <%_ if (auth.includes('JWT')) { _%>
65
+ <%_ const authPath = "/api/auth"; _%>
65
66
  it('should login and obtain a JWT token', async () => {
66
67
  const response = await request(SERVER_URL)
67
- .post('/api/auth/login')
68
+ .post('<%= authPath %>/login')
68
69
  .send({ email: uniqueEmail, password: testPassword });
69
70
 
70
71
  expect(response.statusCode).toBe(200);
71
72
  expect(response.body.accessToken || response.body.token).toBeDefined();
72
73
  authToken = response.body.accessToken || response.body.token;
73
74
  });
75
+
76
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
77
+ it('should fail social exchange with invalid code', async () => {
78
+ const response = await request(SERVER_URL)
79
+ .post('<%= authPath %>/social/exchange')
80
+ .send({ code: 'invalid_code', provider: 'Google' });
81
+
82
+ expect([401, 500]).toContain(response.statusCode);
83
+ });
84
+
85
+ <% } %>
74
86
  <%_ } _%>
75
87
 
76
88
  <%_ if (communication === 'GraphQL') { -%>
@@ -61,15 +61,27 @@ describe('E2E User Tests', () => {
61
61
  <%_ } -%>
62
62
 
63
63
  <%_ if (auth.includes('JWT')) { _%>
64
+ <%_ const authPath = "/api/auth"; _%>
64
65
  it('should login and obtain a JWT token', async () => {
65
66
  const response = await request(SERVER_URL)
66
- .post('/api/auth/login')
67
+ .post('<%= authPath %>/login')
67
68
  .send({ email: uniqueEmail, password: testPassword });
68
69
 
69
70
  expect(response.statusCode).toBe(200);
70
71
  expect(response.body.accessToken || response.body.token).toBeDefined();
71
72
  authToken = response.body.accessToken || response.body.token;
72
73
  });
74
+
75
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
76
+ it('should fail social exchange with invalid code', async () => {
77
+ const response = await request(SERVER_URL)
78
+ .post('<%= authPath %>/social/exchange')
79
+ .send({ code: 'invalid_code', provider: 'Google' });
80
+
81
+ expect([401, 500]).toContain(response.statusCode);
82
+ });
83
+
84
+ <% } %>
73
85
  <%_ } _%>
74
86
 
75
87
  <%_ if (communication === 'GraphQL') { -%>
@@ -50,7 +50,8 @@ tags:
50
50
  <%_ } -%>
51
51
  paths:
52
52
  <%_ if (auth.includes('JWT')) { -%>
53
- /api/auth/login:
53
+ <%_ const authPath = "/api/auth"; _%>
54
+ <%= authPath %>/login:
54
55
  post:
55
56
  summary: Login and receive a JWT token
56
57
  tags:
@@ -85,7 +86,7 @@ paths:
85
86
  type: string
86
87
  '401':
87
88
  description: Invalid credentials
88
- /api/auth/refresh:
89
+ <%= authPath %>/refresh:
89
90
  post:
90
91
  summary: Refresh access token using a refresh token
91
92
  tags:
@@ -115,7 +116,7 @@ paths:
115
116
  type: string
116
117
  '401':
117
118
  description: Invalid refresh token
118
- /api/auth/logout:
119
+ <%= authPath %>/logout:
119
120
  post:
120
121
  summary: Logout and revoke refresh token
121
122
  tags:
@@ -140,6 +141,64 @@ paths:
140
141
  properties:
141
142
  message:
142
143
  type: string
144
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
145
+ <%= authPath %>/social/exchange:
146
+ post:
147
+ summary: Exchange social auth code for JWT tokens
148
+ tags:
149
+ - Auth
150
+ requestBody:
151
+ required: true
152
+ content:
153
+ application/json:
154
+ schema:
155
+ type: object
156
+ required:
157
+ - code
158
+ - provider
159
+ properties:
160
+ code:
161
+ type: string
162
+ provider:
163
+ type: string
164
+ enum: [Google, GitHub]
165
+ redirectUri:
166
+ type: string
167
+ responses:
168
+ '200':
169
+ description: Exchange successful
170
+ content:
171
+ application/json:
172
+ schema:
173
+ type: object
174
+ properties:
175
+ accessToken:
176
+ type: string
177
+ refreshToken:
178
+ type: string
179
+ '401':
180
+ description: Invalid social profile or code
181
+ <%_ if (socialAuth.includes('Google')) { -%>
182
+ <%= authPath %>/google:
183
+ get:
184
+ summary: Redirect to Google OAuth
185
+ tags:
186
+ - Auth
187
+ responses:
188
+ '302':
189
+ description: Redirect to Google
190
+ <%_ } -%>
191
+ <%_ if (socialAuth.includes('GitHub')) { -%>
192
+ <%= authPath %>/github:
193
+ get:
194
+ summary: Redirect to GitHub OAuth
195
+ tags:
196
+ - Auth
197
+ responses:
198
+ '302':
199
+ description: Redirect to GitHub
200
+ <%_ } -%>
201
+ <%_ } -%>
143
202
  <%_ } -%>
144
203
  /api/users:
145
204
  get:
@@ -209,6 +209,63 @@
209
209
  border-radius: 20px;
210
210
  }
211
211
  }
212
+
213
+ .divider {
214
+ display: flex;
215
+ align-items: center;
216
+ text-align: center;
217
+ margin: 24px 0;
218
+ color: var(--text-muted);
219
+ font-size: 13px;
220
+ }
221
+
222
+ .divider::before,
223
+ .divider::after {
224
+ content: '';
225
+ flex: 1;
226
+ border-bottom: 1px solid var(--card-border);
227
+ }
228
+
229
+ .divider:not(:empty)::before {
230
+ margin-right: .5em;
231
+ }
232
+
233
+ .divider:not(:empty)::after {
234
+ margin-left: .5em;
235
+ }
236
+
237
+ .social-login {
238
+ display: grid;
239
+ grid-template-columns: 1fr 1fr;
240
+ gap: 12px;
241
+ }
242
+
243
+ .btn-social {
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ gap: 8px;
248
+ background: rgba(255, 255, 255, 0.05);
249
+ border: 1px solid var(--card-border);
250
+ border-radius: 12px;
251
+ padding: 12px;
252
+ color: #fff;
253
+ font-size: 14px;
254
+ font-weight: 500;
255
+ cursor: pointer;
256
+ transition: all 0.3s ease;
257
+ text-decoration: none;
258
+ }
259
+
260
+ .btn-social:hover {
261
+ background: rgba(255, 255, 255, 0.08);
262
+ border-color: var(--text-muted);
263
+ transform: translateY(-1px);
264
+ }
265
+
266
+ .social-full {
267
+ grid-column: span 2;
268
+ }
212
269
  </style>
213
270
  </head>
214
271
  <body>
@@ -235,6 +292,33 @@
235
292
  <button type="submit" class="btn-primary">Sign In</button>
236
293
  </form>
237
294
 
295
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { %>
296
+ <div class="divider">Or continue with</div>
297
+
298
+ <div class="social-login">
299
+ <% if (socialAuth.includes('Google')) { %>
300
+ <a href="/api/auth/google" class="btn-social <%= socialAuth.includes('GitHub') ? '' : 'social-full' %>">
301
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
302
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
303
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
304
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
305
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.66l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
306
+ </svg>
307
+ Google
308
+ </a>
309
+ <% } %>
310
+
311
+ <% if (socialAuth.includes('GitHub')) { %>
312
+ <a href="/api/auth/github" class="btn-social <%= socialAuth.includes('Google') ? '' : 'social-full' %>">
313
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
314
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
315
+ </svg>
316
+ GitHub
317
+ </a>
318
+ <% } %>
319
+ </div>
320
+ <% } %>
321
+
238
322
  <div class="footer">
239
323
  <p>Don't have an account? <a href="/signup">Create account</a></p>
240
324
  <a href="/" class="back-link">← Back to home</a>
@@ -200,6 +200,63 @@
200
200
  padding: 35px 20px;
201
201
  }
202
202
  }
203
+
204
+ .divider {
205
+ display: flex;
206
+ align-items: center;
207
+ text-align: center;
208
+ margin: 24px 0;
209
+ color: var(--text-muted);
210
+ font-size: 13px;
211
+ }
212
+
213
+ .divider::before,
214
+ .divider::after {
215
+ content: '';
216
+ flex: 1;
217
+ border-bottom: 1px solid var(--card-border);
218
+ }
219
+
220
+ .divider:not(:empty)::before {
221
+ margin-right: .5em;
222
+ }
223
+
224
+ .divider:not(:empty)::after {
225
+ margin-left: .5em;
226
+ }
227
+
228
+ .social-login {
229
+ display: grid;
230
+ grid-template-columns: 1fr 1fr;
231
+ gap: 12px;
232
+ }
233
+
234
+ .btn-social {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ gap: 8px;
239
+ background: rgba(255, 255, 255, 0.05);
240
+ border: 1px solid var(--card-border);
241
+ border-radius: 12px;
242
+ padding: 12px;
243
+ color: #fff;
244
+ font-size: 14px;
245
+ font-weight: 500;
246
+ cursor: pointer;
247
+ transition: all 0.3s ease;
248
+ text-decoration: none;
249
+ }
250
+
251
+ .btn-social:hover {
252
+ background: rgba(255, 255, 255, 0.08);
253
+ border-color: var(--text-muted);
254
+ transform: translateY(-1px);
255
+ }
256
+
257
+ .social-full {
258
+ grid-column: span 2;
259
+ }
203
260
  </style>
204
261
  </head>
205
262
  <body>
@@ -231,6 +288,33 @@
231
288
  <button type="submit" class="btn-primary">Create Account</button>
232
289
  </form>
233
290
 
291
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { %>
292
+ <div class="divider">Or join with</div>
293
+
294
+ <div class="social-login">
295
+ <% if (socialAuth.includes('Google')) { %>
296
+ <a href="/api/auth/google" class="btn-social <%= socialAuth.includes('GitHub') ? '' : 'social-full' %>">
297
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
298
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
299
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
300
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
301
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.66l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
302
+ </svg>
303
+ Google
304
+ </a>
305
+ <% } %>
306
+
307
+ <% if (socialAuth.includes('GitHub')) { %>
308
+ <a href="/api/auth/github" class="btn-social <%= socialAuth.includes('Google') ? '' : 'social-full' %>">
309
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
310
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
311
+ </svg>
312
+ GitHub
313
+ </a>
314
+ <% } %>
315
+ </div>
316
+ <% } %>
317
+
234
318
  <div class="footer">
235
319
  <p>Already have an account? <a href="/login">Sign in</a></p>
236
320
  <a href="/" class="back-home">← Back to home</a>
@@ -174,6 +174,63 @@ html(lang="en")
174
174
  font-size: 13px;
175
175
  opacity: 0.6;
176
176
  }
177
+
178
+ .divider {
179
+ display: flex;
180
+ align-items: center;
181
+ text-align: center;
182
+ margin: 24px 0;
183
+ color: var(--text-muted);
184
+ font-size: 13px;
185
+ }
186
+
187
+ .divider::before,
188
+ .divider::after {
189
+ content: '';
190
+ flex: 1;
191
+ border-bottom: 1px solid var(--card-border);
192
+ }
193
+
194
+ .divider:not(:empty)::before {
195
+ margin-right: .5em;
196
+ }
197
+
198
+ .divider:not(:empty)::after {
199
+ margin-left: .5em;
200
+ }
201
+
202
+ .social-login {
203
+ display: grid;
204
+ grid-template-columns: 1fr 1fr;
205
+ gap: 12px;
206
+ }
207
+
208
+ .btn-social {
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ gap: 8px;
213
+ background: rgba(255, 255, 255, 0.05);
214
+ border: 1px solid var(--card-border);
215
+ border-radius: 12px;
216
+ padding: 12px;
217
+ color: #fff;
218
+ font-size: 14px;
219
+ font-weight: 500;
220
+ cursor: pointer;
221
+ transition: all 0.3s ease;
222
+ text-decoration: none;
223
+ }
224
+
225
+ .btn-social:hover {
226
+ background: rgba(255, 255, 255, 0.08);
227
+ border-color: var(--text-muted);
228
+ transform: translateY(-1px);
229
+ }
230
+
231
+ .social-full {
232
+ grid-column: span 2;
233
+ }
177
234
  body
178
235
  div.background-blobs
179
236
  div.blob.blob-1
@@ -189,6 +246,27 @@ html(lang="en")
189
246
  label(for="password") Password
190
247
  input(type="password" id="password" name="password" placeholder="••••••••" required)
191
248
  button.btn-primary(type="submit") Sign In
249
+
250
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { %>
251
+ div.divider Or continue with
252
+ div.social-login
253
+ <% if (socialAuth.includes('Google')) { %>
254
+ a.btn-social(href="/api/auth/google" class="<%= socialAuth.includes('GitHub') ? '' : 'social-full' %>")
255
+ svg(width="20" height="20" viewBox="0 0 24 24" fill="currentColor")
256
+ path(d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4")
257
+ path(d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853")
258
+ path(d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05")
259
+ path(d="M12 5.38c1.62 0 3.06.56 4.21 1.66l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335")
260
+ | Google
261
+ <% } %>
262
+ <% if (socialAuth.includes('GitHub')) { %>
263
+ a.btn-social(href="/api/auth/github" class="<%= socialAuth.includes('Google') ? '' : 'social-full' %>")
264
+ svg(width="20" height="20" viewBox="0 0 24 24" fill="currentColor")
265
+ path(d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12")
266
+ | GitHub
267
+ <% } %>
268
+ <% } %>
269
+
192
270
  div.footer
193
271
  p Don't have an account?
194
272
  a(href="/signup") Create account