go-gin-cli 1.0.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/README.md +287 -0
- package/git-manager.sh +495 -0
- package/package.json +36 -0
- package/prompt.md +6 -0
- package/src/bin/index.js +410 -0
- package/src/lib/constants/index.js +24 -0
- package/src/lib/shares/createDir.js +22 -0
- package/src/lib/shares/createFile.js +23 -0
- package/src/lib/utils/add-auth-to-resource.js +225 -0
- package/src/lib/utils/create-auth.js +937 -0
- package/src/lib/utils/create-resource.js +1426 -0
- package/src/lib/utils/create-service.js +456 -0
- package/src/lib/utils/display.js +19 -0
- package/src/lib/utils/help.js +93 -0
- package/src/lib/utils/remove-module.js +146 -0
- package/src/lib/utils/setup.js +1626 -0
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs").promises;
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const createDir = require("../shares/createDir");
|
|
6
|
+
const createFile = require("../shares/createFile");
|
|
7
|
+
const { COLORS } = require("../constants/index");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* AuthGenerator - Generates JWT and Passport authentication files for Go Clean Architecture
|
|
11
|
+
*
|
|
12
|
+
* Structure:
|
|
13
|
+
* src/
|
|
14
|
+
* ├── domain/
|
|
15
|
+
* │ └── models/auth.go
|
|
16
|
+
* ├── infrastructure/
|
|
17
|
+
* │ ├── common/
|
|
18
|
+
* │ │ ├── auth/
|
|
19
|
+
* │ │ │ ├── jwt.go
|
|
20
|
+
* │ │ │ ├── passport.go
|
|
21
|
+
* │ │ │ └── middleware.go
|
|
22
|
+
* │ │ └── guards/
|
|
23
|
+
* │ │ └── auth_guard.go
|
|
24
|
+
* │ └── repositories/auth/
|
|
25
|
+
* │ └── auth.repository.go
|
|
26
|
+
* └── usecases/auth/
|
|
27
|
+
* └── auth.usecase.go
|
|
28
|
+
*/
|
|
29
|
+
class AuthGenerator {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.srcDir = "src";
|
|
32
|
+
this.domainDir = path.join(this.srcDir, "domain");
|
|
33
|
+
this.infrastructureDir = path.join(this.srcDir, "infrastructure");
|
|
34
|
+
this.usecasesDir = path.join(this.srcDir, "usecases");
|
|
35
|
+
|
|
36
|
+
// Get go module name from go.mod
|
|
37
|
+
this.goModule = this.getGoModuleName();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getGoModuleName() {
|
|
41
|
+
try {
|
|
42
|
+
const goModPath = path.join(process.cwd(), "go.mod");
|
|
43
|
+
const content = require("fs").readFileSync(goModPath, "utf8");
|
|
44
|
+
const match = content.match(/module\s+(.+)/);
|
|
45
|
+
return match ? match[1].trim() : "github.com/example/project";
|
|
46
|
+
} catch {
|
|
47
|
+
return "github.com/example/project";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async execute() {
|
|
52
|
+
try {
|
|
53
|
+
console.log(`${COLORS.YELLOW}Starting authentication generation...${COLORS.NC}`);
|
|
54
|
+
|
|
55
|
+
// Check if auth already exists
|
|
56
|
+
const authDir = path.join(this.infrastructureDir, "common", "auth");
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(authDir);
|
|
59
|
+
console.log(`${COLORS.YELLOW}⚠ Authentication module already exists${COLORS.NC}`);
|
|
60
|
+
return;
|
|
61
|
+
} catch {
|
|
62
|
+
// Directory doesn't exist, continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await this.setupDirectories();
|
|
66
|
+
await this.generateDomainFiles();
|
|
67
|
+
await this.generateInfrastructureFiles();
|
|
68
|
+
await this.generateUsecaseFiles();
|
|
69
|
+
await this.updateGoMod();
|
|
70
|
+
|
|
71
|
+
console.log(`${COLORS.GREEN}Authentication generation completed successfully!${COLORS.NC}`);
|
|
72
|
+
this.printSummary();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`${COLORS.RED}Error during generation:${COLORS.NC} ${error.message}`);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async setupDirectories() {
|
|
80
|
+
const dirs = [
|
|
81
|
+
path.join(this.infrastructureDir, "common", "auth"),
|
|
82
|
+
path.join(this.infrastructureDir, "repositories", "auth"),
|
|
83
|
+
path.join(this.usecasesDir, "auth"),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
await createDir(dir);
|
|
88
|
+
}
|
|
89
|
+
console.log(`${COLORS.GREEN}✔ Created directories${COLORS.NC}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async generateDomainFiles() {
|
|
93
|
+
// Auth model
|
|
94
|
+
await createFile(
|
|
95
|
+
path.join(this.domainDir, "models", "auth.go"),
|
|
96
|
+
this.getAuthModelContent()
|
|
97
|
+
);
|
|
98
|
+
console.log(`${COLORS.GREEN}✔ Created domain/models/auth.go${COLORS.NC}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async generateInfrastructureFiles() {
|
|
102
|
+
// JWT utility
|
|
103
|
+
await createFile(
|
|
104
|
+
path.join(this.infrastructureDir, "common", "auth", "jwt.go"),
|
|
105
|
+
this.getJwtContent()
|
|
106
|
+
);
|
|
107
|
+
console.log(`${COLORS.GREEN}✔ Created infrastructure/common/auth/jwt.go${COLORS.NC}`);
|
|
108
|
+
|
|
109
|
+
// Passport (strategies)
|
|
110
|
+
await createFile(
|
|
111
|
+
path.join(this.infrastructureDir, "common", "auth", "passport.go"),
|
|
112
|
+
this.getPassportContent()
|
|
113
|
+
);
|
|
114
|
+
console.log(`${COLORS.GREEN}✔ Created infrastructure/common/auth/passport.go${COLORS.NC}`);
|
|
115
|
+
|
|
116
|
+
// Auth middleware
|
|
117
|
+
await createFile(
|
|
118
|
+
path.join(this.infrastructureDir, "common", "auth", "middleware.go"),
|
|
119
|
+
this.getMiddlewareContent()
|
|
120
|
+
);
|
|
121
|
+
console.log(`${COLORS.GREEN}✔ Created infrastructure/common/auth/middleware.go${COLORS.NC}`);
|
|
122
|
+
|
|
123
|
+
// Auth middleware for Gin
|
|
124
|
+
await createFile(
|
|
125
|
+
path.join(this.infrastructureDir, "common", "middleware", "auth.go"),
|
|
126
|
+
this.getAuthMiddlewareGinContent()
|
|
127
|
+
);
|
|
128
|
+
console.log(`${COLORS.GREEN}✔ Created infrastructure/common/middleware/auth.go${COLORS.NC}`);
|
|
129
|
+
|
|
130
|
+
// Auth repository
|
|
131
|
+
await createFile(
|
|
132
|
+
path.join(this.infrastructureDir, "repositories", "auth", "auth.repository.go"),
|
|
133
|
+
this.getAuthRepositoryContent()
|
|
134
|
+
);
|
|
135
|
+
console.log(`${COLORS.GREEN}✔ Created infrastructure/repositories/auth/auth.repository.go${COLORS.NC}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async generateUsecaseFiles() {
|
|
139
|
+
await createFile(
|
|
140
|
+
path.join(this.usecasesDir, "auth", "auth.usecase.go"),
|
|
141
|
+
this.getAuthUsecaseContent()
|
|
142
|
+
);
|
|
143
|
+
console.log(`${COLORS.GREEN}✔ Created usecases/auth/auth.usecase.go${COLORS.NC}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async updateGoMod() {
|
|
147
|
+
const goModPath = path.join(process.cwd(), "go.mod");
|
|
148
|
+
try {
|
|
149
|
+
let content = await fs.readFile(goModPath, "utf8");
|
|
150
|
+
|
|
151
|
+
// Add JWT dependency if not present
|
|
152
|
+
if (!content.includes("github.com/golang-jwt/jwt/v5")) {
|
|
153
|
+
content = content.replace(
|
|
154
|
+
/require \(/,
|
|
155
|
+
`require (\n\tgithub.com/golang-jwt/jwt/v5 v5.2.0`
|
|
156
|
+
);
|
|
157
|
+
await fs.writeFile(goModPath, content, "utf8");
|
|
158
|
+
console.log(`${COLORS.GREEN}✔ Added JWT dependency to go.mod${COLORS.NC}`);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.log(`${COLORS.YELLOW}⚠ Could not update go.mod: ${error.message}${COLORS.NC}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
printSummary() {
|
|
166
|
+
console.log(`
|
|
167
|
+
${COLORS.CYAN}╔════════════════════════════════════════════════════════════╗
|
|
168
|
+
║ Authentication Generated ║
|
|
169
|
+
╠════════════════════════════════════════════════════════════╣
|
|
170
|
+
║ 📁 Files created: ║
|
|
171
|
+
║ • domain/models/auth.go ║
|
|
172
|
+
║ • infrastructure/common/auth/jwt.go ║
|
|
173
|
+
║ • infrastructure/common/auth/passport.go ║
|
|
174
|
+
║ • infrastructure/common/auth/middleware.go ║
|
|
175
|
+
║ • infrastructure/common/middleware/auth.go ║
|
|
176
|
+
║ • infrastructure/repositories/auth/auth.repository.go ║
|
|
177
|
+
║ • usecases/auth/auth.usecase.go ║
|
|
178
|
+
╠════════════════════════════════════════════════════════════╣
|
|
179
|
+
║ 🔧 Next steps: ║
|
|
180
|
+
║ 1. Set JWT_SECRET in your .env file ║
|
|
181
|
+
║ 2. Run: go mod tidy ║
|
|
182
|
+
║ 3. Use auth.Usecase in your services ║
|
|
183
|
+
╚════════════════════════════════════════════════════════════╝${COLORS.NC}
|
|
184
|
+
`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================
|
|
188
|
+
// Content Generators
|
|
189
|
+
// ============================================
|
|
190
|
+
|
|
191
|
+
getAuthModelContent() {
|
|
192
|
+
return `package models
|
|
193
|
+
|
|
194
|
+
import (
|
|
195
|
+
"time"
|
|
196
|
+
|
|
197
|
+
"github.com/google/uuid"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// AuthUser represents the authenticated user
|
|
201
|
+
type AuthUser struct {
|
|
202
|
+
ID uuid.UUID \`json:"id"\`
|
|
203
|
+
Email string \`json:"email"\`
|
|
204
|
+
Role string \`json:"role"\`
|
|
205
|
+
IsActive bool \`json:"is_active"\`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// TokenPair represents access and refresh tokens
|
|
209
|
+
type TokenPair struct {
|
|
210
|
+
AccessToken string \`json:"access_token"\`
|
|
211
|
+
RefreshToken string \`json:"refresh_token"\`
|
|
212
|
+
ExpiresIn int64 \`json:"expires_in"\`
|
|
213
|
+
TokenType string \`json:"token_type"\`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// TokenClaims represents JWT claims
|
|
217
|
+
type TokenClaims struct {
|
|
218
|
+
UserID uuid.UUID \`json:"user_id"\`
|
|
219
|
+
Email string \`json:"email"\`
|
|
220
|
+
Role string \`json:"role"\`
|
|
221
|
+
TokenType string \`json:"token_type"\`
|
|
222
|
+
IssuedAt time.Time \`json:"iat"\`
|
|
223
|
+
ExpiresAt time.Time \`json:"exp"\`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// LoginRequest represents login credentials
|
|
227
|
+
type LoginRequest struct {
|
|
228
|
+
Email string \`json:"email"\`
|
|
229
|
+
Password string \`json:"password"\`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// RegisterRequest represents registration data
|
|
233
|
+
type RegisterRequest struct {
|
|
234
|
+
Email string \`json:"email"\`
|
|
235
|
+
Password string \`json:"password"\`
|
|
236
|
+
Name string \`json:"name"\`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// RefreshTokenRequest represents refresh token request
|
|
240
|
+
type RefreshTokenRequest struct {
|
|
241
|
+
RefreshToken string \`json:"refresh_token"\`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// AuthContextKey is the context key for auth user
|
|
245
|
+
type AuthContextKey string
|
|
246
|
+
|
|
247
|
+
// AuthUserKey is the context key for authenticated user
|
|
248
|
+
const AuthUserKey AuthContextKey = "authUser"
|
|
249
|
+
|
|
250
|
+
// TokenClaimsKey is the context key for token claims
|
|
251
|
+
const TokenClaimsKey AuthContextKey = "tokenClaims"
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getJwtContent() {
|
|
256
|
+
return `package auth
|
|
257
|
+
|
|
258
|
+
import (
|
|
259
|
+
"errors"
|
|
260
|
+
"os"
|
|
261
|
+
"time"
|
|
262
|
+
|
|
263
|
+
"${this.goModule}/src/domain/models"
|
|
264
|
+
|
|
265
|
+
"github.com/golang-jwt/jwt/v5"
|
|
266
|
+
"github.com/google/uuid"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
var (
|
|
270
|
+
ErrInvalidToken = errors.New("invalid token")
|
|
271
|
+
ErrExpiredToken = errors.New("token has expired")
|
|
272
|
+
ErrInvalidSignature = errors.New("invalid token signature")
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// JWTConfig holds JWT configuration
|
|
276
|
+
type JWTConfig struct {
|
|
277
|
+
SecretKey string
|
|
278
|
+
AccessTokenDuration time.Duration
|
|
279
|
+
RefreshTokenDuration time.Duration
|
|
280
|
+
Issuer string
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// JWTService handles JWT operations
|
|
284
|
+
type JWTService struct {
|
|
285
|
+
config JWTConfig
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// NewJWTService creates a new JWT service
|
|
289
|
+
func NewJWTService() *JWTService {
|
|
290
|
+
secret := os.Getenv("JWT_SECRET")
|
|
291
|
+
if secret == "" {
|
|
292
|
+
secret = "your-super-secret-key-change-in-production"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return &JWTService{
|
|
296
|
+
config: JWTConfig{
|
|
297
|
+
SecretKey: secret,
|
|
298
|
+
AccessTokenDuration: 15 * time.Minute,
|
|
299
|
+
RefreshTokenDuration: 7 * 24 * time.Hour,
|
|
300
|
+
Issuer: os.Getenv("JWT_ISSUER"),
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// CustomClaims represents the JWT claims
|
|
306
|
+
type CustomClaims struct {
|
|
307
|
+
UserID string \`json:"user_id"\`
|
|
308
|
+
Email string \`json:"email"\`
|
|
309
|
+
Role string \`json:"role"\`
|
|
310
|
+
TokenType string \`json:"token_type"\`
|
|
311
|
+
jwt.RegisteredClaims
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// GenerateTokenPair generates access and refresh tokens
|
|
315
|
+
func (s *JWTService) GenerateTokenPair(user *models.AuthUser) (*models.TokenPair, error) {
|
|
316
|
+
accessToken, err := s.generateToken(user, "access", s.config.AccessTokenDuration)
|
|
317
|
+
if err != nil {
|
|
318
|
+
return nil, err
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
refreshToken, err := s.generateToken(user, "refresh", s.config.RefreshTokenDuration)
|
|
322
|
+
if err != nil {
|
|
323
|
+
return nil, err
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return &models.TokenPair{
|
|
327
|
+
AccessToken: accessToken,
|
|
328
|
+
RefreshToken: refreshToken,
|
|
329
|
+
ExpiresIn: int64(s.config.AccessTokenDuration.Seconds()),
|
|
330
|
+
TokenType: "Bearer",
|
|
331
|
+
}, nil
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// generateToken creates a new JWT token
|
|
335
|
+
func (s *JWTService) generateToken(user *models.AuthUser, tokenType string, duration time.Duration) (string, error) {
|
|
336
|
+
now := time.Now()
|
|
337
|
+
claims := CustomClaims{
|
|
338
|
+
UserID: user.ID.String(),
|
|
339
|
+
Email: user.Email,
|
|
340
|
+
Role: user.Role,
|
|
341
|
+
TokenType: tokenType,
|
|
342
|
+
RegisteredClaims: jwt.RegisteredClaims{
|
|
343
|
+
Issuer: s.config.Issuer,
|
|
344
|
+
Subject: user.ID.String(),
|
|
345
|
+
IssuedAt: jwt.NewNumericDate(now),
|
|
346
|
+
ExpiresAt: jwt.NewNumericDate(now.Add(duration)),
|
|
347
|
+
NotBefore: jwt.NewNumericDate(now),
|
|
348
|
+
},
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
352
|
+
return token.SignedString([]byte(s.config.SecretKey))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ValidateToken validates a JWT token and returns claims
|
|
356
|
+
func (s *JWTService) ValidateToken(tokenString string) (*models.TokenClaims, error) {
|
|
357
|
+
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
358
|
+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
359
|
+
return nil, ErrInvalidSignature
|
|
360
|
+
}
|
|
361
|
+
return []byte(s.config.SecretKey), nil
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
if err != nil {
|
|
365
|
+
if errors.Is(err, jwt.ErrTokenExpired) {
|
|
366
|
+
return nil, ErrExpiredToken
|
|
367
|
+
}
|
|
368
|
+
return nil, ErrInvalidToken
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
claims, ok := token.Claims.(*CustomClaims)
|
|
372
|
+
if !ok || !token.Valid {
|
|
373
|
+
return nil, ErrInvalidToken
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
userID, err := uuid.Parse(claims.UserID)
|
|
377
|
+
if err != nil {
|
|
378
|
+
return nil, ErrInvalidToken
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return &models.TokenClaims{
|
|
382
|
+
UserID: userID,
|
|
383
|
+
Email: claims.Email,
|
|
384
|
+
Role: claims.Role,
|
|
385
|
+
TokenType: claims.TokenType,
|
|
386
|
+
IssuedAt: claims.IssuedAt.Time,
|
|
387
|
+
ExpiresAt: claims.ExpiresAt.Time,
|
|
388
|
+
}, nil
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// RefreshAccessToken generates a new access token from a valid refresh token
|
|
392
|
+
func (s *JWTService) RefreshAccessToken(refreshToken string) (*models.TokenPair, error) {
|
|
393
|
+
claims, err := s.ValidateToken(refreshToken)
|
|
394
|
+
if err != nil {
|
|
395
|
+
return nil, err
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if claims.TokenType != "refresh" {
|
|
399
|
+
return nil, ErrInvalidToken
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
user := &models.AuthUser{
|
|
403
|
+
ID: claims.UserID,
|
|
404
|
+
Email: claims.Email,
|
|
405
|
+
Role: claims.Role,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return s.GenerateTokenPair(user)
|
|
409
|
+
}
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
getPassportContent() {
|
|
414
|
+
return `package auth
|
|
415
|
+
|
|
416
|
+
import (
|
|
417
|
+
"context"
|
|
418
|
+
"errors"
|
|
419
|
+
|
|
420
|
+
"${this.goModule}/src/domain/models"
|
|
421
|
+
|
|
422
|
+
"golang.org/x/crypto/bcrypt"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
var (
|
|
426
|
+
ErrInvalidCredentials = errors.New("invalid credentials")
|
|
427
|
+
ErrUserNotFound = errors.New("user not found")
|
|
428
|
+
ErrUserInactive = errors.New("user account is inactive")
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
// Strategy defines the authentication strategy interface
|
|
432
|
+
type Strategy interface {
|
|
433
|
+
Authenticate(ctx context.Context, credentials interface{}) (*models.AuthUser, error)
|
|
434
|
+
Name() string
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Passport manages authentication strategies
|
|
438
|
+
type Passport struct {
|
|
439
|
+
strategies map[string]Strategy
|
|
440
|
+
jwtService *JWTService
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// NewPassport creates a new passport instance
|
|
444
|
+
func NewPassport(jwtService *JWTService) *Passport {
|
|
445
|
+
return &Passport{
|
|
446
|
+
strategies: make(map[string]Strategy),
|
|
447
|
+
jwtService: jwtService,
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// RegisterStrategy registers an authentication strategy
|
|
452
|
+
func (p *Passport) RegisterStrategy(strategy Strategy) {
|
|
453
|
+
p.strategies[strategy.Name()] = strategy
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Authenticate authenticates using the specified strategy
|
|
457
|
+
func (p *Passport) Authenticate(ctx context.Context, strategyName string, credentials interface{}) (*models.AuthUser, error) {
|
|
458
|
+
strategy, ok := p.strategies[strategyName]
|
|
459
|
+
if !ok {
|
|
460
|
+
return nil, errors.New("unknown authentication strategy: " + strategyName)
|
|
461
|
+
}
|
|
462
|
+
return strategy.Authenticate(ctx, credentials)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// GenerateTokens generates JWT tokens for the authenticated user
|
|
466
|
+
func (p *Passport) GenerateTokens(user *models.AuthUser) (*models.TokenPair, error) {
|
|
467
|
+
return p.jwtService.GenerateTokenPair(user)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// Local Strategy (Email/Password)
|
|
472
|
+
// ============================================================================
|
|
473
|
+
|
|
474
|
+
// UserFinder interface for finding users
|
|
475
|
+
type UserFinder interface {
|
|
476
|
+
FindByEmail(ctx context.Context, email string) (*models.AuthUser, string, error) // returns user, hashedPassword, error
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// LocalStrategy implements email/password authentication
|
|
480
|
+
type LocalStrategy struct {
|
|
481
|
+
userFinder UserFinder
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// NewLocalStrategy creates a new local strategy
|
|
485
|
+
func NewLocalStrategy(userFinder UserFinder) *LocalStrategy {
|
|
486
|
+
return &LocalStrategy{
|
|
487
|
+
userFinder: userFinder,
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Name returns the strategy name
|
|
492
|
+
func (s *LocalStrategy) Name() string {
|
|
493
|
+
return "local"
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Authenticate validates email and password
|
|
497
|
+
func (s *LocalStrategy) Authenticate(ctx context.Context, credentials interface{}) (*models.AuthUser, error) {
|
|
498
|
+
creds, ok := credentials.(*models.LoginRequest)
|
|
499
|
+
if !ok {
|
|
500
|
+
return nil, ErrInvalidCredentials
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
user, hashedPassword, err := s.userFinder.FindByEmail(ctx, creds.Email)
|
|
504
|
+
if err != nil {
|
|
505
|
+
return nil, ErrUserNotFound
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if !user.IsActive {
|
|
509
|
+
return nil, ErrUserInactive
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(creds.Password)); err != nil {
|
|
513
|
+
return nil, ErrInvalidCredentials
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return user, nil
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ============================================================================
|
|
520
|
+
// JWT Strategy (Token validation)
|
|
521
|
+
// ============================================================================
|
|
522
|
+
|
|
523
|
+
// JWTStrategy implements JWT token authentication
|
|
524
|
+
type JWTStrategy struct {
|
|
525
|
+
jwtService *JWTService
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// NewJWTStrategy creates a new JWT strategy
|
|
529
|
+
func NewJWTStrategy(jwtService *JWTService) *JWTStrategy {
|
|
530
|
+
return &JWTStrategy{
|
|
531
|
+
jwtService: jwtService,
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Name returns the strategy name
|
|
536
|
+
func (s *JWTStrategy) Name() string {
|
|
537
|
+
return "jwt"
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Authenticate validates JWT token
|
|
541
|
+
func (s *JWTStrategy) Authenticate(ctx context.Context, credentials interface{}) (*models.AuthUser, error) {
|
|
542
|
+
token, ok := credentials.(string)
|
|
543
|
+
if !ok {
|
|
544
|
+
return nil, ErrInvalidToken
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
claims, err := s.jwtService.ValidateToken(token)
|
|
548
|
+
if err != nil {
|
|
549
|
+
return nil, err
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return &models.AuthUser{
|
|
553
|
+
ID: claims.UserID,
|
|
554
|
+
Email: claims.Email,
|
|
555
|
+
Role: claims.Role,
|
|
556
|
+
}, nil
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ============================================================================
|
|
560
|
+
// Password Utilities
|
|
561
|
+
// ============================================================================
|
|
562
|
+
|
|
563
|
+
// HashPassword hashes a password using bcrypt
|
|
564
|
+
func HashPassword(password string) (string, error) {
|
|
565
|
+
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
566
|
+
return string(bytes), err
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// CheckPassword compares a password with its hash
|
|
570
|
+
func CheckPassword(password, hash string) bool {
|
|
571
|
+
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
|
572
|
+
return err == nil
|
|
573
|
+
}
|
|
574
|
+
`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
getMiddlewareContent() {
|
|
578
|
+
return `package auth
|
|
579
|
+
|
|
580
|
+
import (
|
|
581
|
+
"context"
|
|
582
|
+
|
|
583
|
+
"${this.goModule}/src/domain/models"
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
// GetAuthUserFromContext retrieves the authenticated user from context
|
|
587
|
+
func GetAuthUserFromContext(ctx context.Context) (*models.AuthUser, bool) {
|
|
588
|
+
user, ok := ctx.Value(models.AuthUserKey).(*models.AuthUser)
|
|
589
|
+
return user, ok
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// GetTokenClaimsFromContext retrieves token claims from context
|
|
593
|
+
func GetTokenClaimsFromContext(ctx context.Context) (*models.TokenClaims, bool) {
|
|
594
|
+
claims, ok := ctx.Value(models.TokenClaimsKey).(*models.TokenClaims)
|
|
595
|
+
return claims, ok
|
|
596
|
+
}
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
getAuthMiddlewareGinContent() {
|
|
601
|
+
return `package middleware
|
|
602
|
+
|
|
603
|
+
import (
|
|
604
|
+
"net/http"
|
|
605
|
+
"strings"
|
|
606
|
+
|
|
607
|
+
"${this.goModule}/src/domain/models"
|
|
608
|
+
"${this.goModule}/src/infrastructure/common/auth"
|
|
609
|
+
"${this.goModule}/src/infrastructure/common/response"
|
|
610
|
+
|
|
611
|
+
"github.com/gin-gonic/gin"
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
// AuthMiddleware provides JWT authentication middleware for Gin
|
|
615
|
+
func AuthMiddleware(jwtService *auth.JWTService) gin.HandlerFunc {
|
|
616
|
+
return func(c *gin.Context) {
|
|
617
|
+
authHeader := c.GetHeader("Authorization")
|
|
618
|
+
if authHeader == "" {
|
|
619
|
+
response.Error(c, http.StatusUnauthorized, "Missing authorization header")
|
|
620
|
+
c.Abort()
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Extract token from "Bearer <token>"
|
|
625
|
+
token := strings.TrimPrefix(authHeader, "Bearer ")
|
|
626
|
+
if token == authHeader {
|
|
627
|
+
response.Error(c, http.StatusUnauthorized, "Invalid authorization format")
|
|
628
|
+
c.Abort()
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Validate token
|
|
633
|
+
claims, err := jwtService.ValidateToken(token)
|
|
634
|
+
if err != nil {
|
|
635
|
+
if err == auth.ErrExpiredToken {
|
|
636
|
+
response.Error(c, http.StatusUnauthorized, "Token expired")
|
|
637
|
+
c.Abort()
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
response.Error(c, http.StatusUnauthorized, "Invalid token")
|
|
641
|
+
c.Abort()
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Create auth user from claims
|
|
646
|
+
authUser := &models.AuthUser{
|
|
647
|
+
ID: claims.UserID,
|
|
648
|
+
Email: claims.Email,
|
|
649
|
+
Role: claims.Role,
|
|
650
|
+
IsActive: true,
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Store user in context
|
|
654
|
+
c.Set(string(models.AuthUserKey), authUser)
|
|
655
|
+
c.Set(string(models.TokenClaimsKey), claims)
|
|
656
|
+
|
|
657
|
+
c.Next()
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// GetAuthUser retrieves the authenticated user from Gin context
|
|
662
|
+
func GetAuthUser(c *gin.Context) (*models.AuthUser, bool) {
|
|
663
|
+
user, exists := c.Get(string(models.AuthUserKey))
|
|
664
|
+
if !exists {
|
|
665
|
+
return nil, false
|
|
666
|
+
}
|
|
667
|
+
authUser, ok := user.(*models.AuthUser)
|
|
668
|
+
return authUser, ok
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// RequireRole middleware checks if user has required role
|
|
672
|
+
func RequireRole(roles ...string) gin.HandlerFunc {
|
|
673
|
+
return func(c *gin.Context) {
|
|
674
|
+
user, ok := GetAuthUser(c)
|
|
675
|
+
if !ok {
|
|
676
|
+
response.Error(c, http.StatusUnauthorized, "Not authenticated")
|
|
677
|
+
c.Abort()
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
for _, role := range roles {
|
|
682
|
+
if user.Role == role {
|
|
683
|
+
c.Next()
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
response.Error(c, http.StatusForbidden, "Insufficient permissions")
|
|
689
|
+
c.Abort()
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
getAuthRepositoryContent() {
|
|
696
|
+
return `package auth
|
|
697
|
+
|
|
698
|
+
import (
|
|
699
|
+
"context"
|
|
700
|
+
|
|
701
|
+
"${this.goModule}/src/domain/models"
|
|
702
|
+
|
|
703
|
+
"github.com/google/uuid"
|
|
704
|
+
"gorm.io/gorm"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
// Repository handles auth data operations
|
|
708
|
+
type Repository struct {
|
|
709
|
+
db *gorm.DB
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// NewRepository creates a new auth repository
|
|
713
|
+
func NewRepository(db *gorm.DB) *Repository {
|
|
714
|
+
return &Repository{db: db}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// UserEntity represents the user in database (example - adjust to your user entity)
|
|
718
|
+
type UserEntity struct {
|
|
719
|
+
models.BaseModel
|
|
720
|
+
Email string \`gorm:"uniqueIndex;not null"\`
|
|
721
|
+
Password string \`gorm:"not null"\`
|
|
722
|
+
Name string
|
|
723
|
+
Role string \`gorm:"default:'user'"\`
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// TableName returns the table name
|
|
727
|
+
func (UserEntity) TableName() string {
|
|
728
|
+
return "users"
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// FindByEmail finds a user by email and returns auth user with hashed password
|
|
732
|
+
func (r *Repository) FindByEmail(ctx context.Context, email string) (*models.AuthUser, string, error) {
|
|
733
|
+
var user UserEntity
|
|
734
|
+
if err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
|
|
735
|
+
return nil, "", err
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
authUser := &models.AuthUser{
|
|
739
|
+
ID: user.ID,
|
|
740
|
+
Email: user.Email,
|
|
741
|
+
Role: user.Role,
|
|
742
|
+
IsActive: user.IsActive,
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return authUser, user.Password, nil
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// FindByID finds a user by ID
|
|
749
|
+
func (r *Repository) FindByID(ctx context.Context, id uuid.UUID) (*models.AuthUser, error) {
|
|
750
|
+
var user UserEntity
|
|
751
|
+
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
|
|
752
|
+
return nil, err
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return &models.AuthUser{
|
|
756
|
+
ID: user.ID,
|
|
757
|
+
Email: user.Email,
|
|
758
|
+
Role: user.Role,
|
|
759
|
+
IsActive: user.IsActive,
|
|
760
|
+
}, nil
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// CreateUser creates a new user
|
|
764
|
+
func (r *Repository) CreateUser(ctx context.Context, email, hashedPassword, name, role string) (*models.AuthUser, error) {
|
|
765
|
+
user := &UserEntity{
|
|
766
|
+
BaseModel: models.NewBaseModel(),
|
|
767
|
+
Email: email,
|
|
768
|
+
Password: hashedPassword,
|
|
769
|
+
Name: name,
|
|
770
|
+
Role: role,
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
|
|
774
|
+
return nil, err
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return &models.AuthUser{
|
|
778
|
+
ID: user.ID,
|
|
779
|
+
Email: user.Email,
|
|
780
|
+
Role: user.Role,
|
|
781
|
+
IsActive: user.IsActive,
|
|
782
|
+
}, nil
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// UpdatePassword updates user password
|
|
786
|
+
func (r *Repository) UpdatePassword(ctx context.Context, userID uuid.UUID, hashedPassword string) error {
|
|
787
|
+
return r.db.WithContext(ctx).Model(&UserEntity{}).
|
|
788
|
+
Where("id = ?", userID).
|
|
789
|
+
Update("password", hashedPassword).Error
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// EmailExists checks if email already exists
|
|
793
|
+
func (r *Repository) EmailExists(ctx context.Context, email string) (bool, error) {
|
|
794
|
+
var count int64
|
|
795
|
+
err := r.db.WithContext(ctx).Model(&UserEntity{}).Where("email = ?", email).Count(&count).Error
|
|
796
|
+
return count > 0, err
|
|
797
|
+
}
|
|
798
|
+
`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
getAuthUsecaseContent() {
|
|
802
|
+
return `package auth
|
|
803
|
+
|
|
804
|
+
import (
|
|
805
|
+
"context"
|
|
806
|
+
"errors"
|
|
807
|
+
|
|
808
|
+
"${this.goModule}/src/domain/logger"
|
|
809
|
+
"${this.goModule}/src/domain/models"
|
|
810
|
+
authPkg "${this.goModule}/src/infrastructure/common/auth"
|
|
811
|
+
authRepo "${this.goModule}/src/infrastructure/repositories/auth"
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
var (
|
|
815
|
+
ErrEmailExists = errors.New("email already exists")
|
|
816
|
+
ErrInvalidPassword = errors.New("password must be at least 8 characters")
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
// Usecase handles authentication business logic
|
|
820
|
+
type Usecase struct {
|
|
821
|
+
repo *authRepo.Repository
|
|
822
|
+
passport *authPkg.Passport
|
|
823
|
+
jwtService *authPkg.JWTService
|
|
824
|
+
logger logger.Logger
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// NewUsecase creates a new auth usecase
|
|
828
|
+
func NewUsecase(repo *authRepo.Repository, logger logger.Logger) *Usecase {
|
|
829
|
+
jwtService := authPkg.NewJWTService()
|
|
830
|
+
passport := authPkg.NewPassport(jwtService)
|
|
831
|
+
|
|
832
|
+
// Register local strategy
|
|
833
|
+
localStrategy := authPkg.NewLocalStrategy(repo)
|
|
834
|
+
passport.RegisterStrategy(localStrategy)
|
|
835
|
+
|
|
836
|
+
// Register JWT strategy
|
|
837
|
+
jwtStrategy := authPkg.NewJWTStrategy(jwtService)
|
|
838
|
+
passport.RegisterStrategy(jwtStrategy)
|
|
839
|
+
|
|
840
|
+
return &Usecase{
|
|
841
|
+
repo: repo,
|
|
842
|
+
passport: passport,
|
|
843
|
+
jwtService: jwtService,
|
|
844
|
+
logger: logger,
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Login authenticates user and returns tokens
|
|
849
|
+
func (u *Usecase) Login(ctx context.Context, req *models.LoginRequest) (*models.TokenPair, error) {
|
|
850
|
+
user, err := u.passport.Authenticate(ctx, "local", req)
|
|
851
|
+
if err != nil {
|
|
852
|
+
u.logger.Error("AuthUsecase", "Login failed", err)
|
|
853
|
+
return nil, err
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
tokens, err := u.passport.GenerateTokens(user)
|
|
857
|
+
if err != nil {
|
|
858
|
+
u.logger.Error("AuthUsecase", "Token generation failed", err)
|
|
859
|
+
return nil, err
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
u.logger.Info("AuthUsecase", "User logged in: "+user.Email)
|
|
863
|
+
return tokens, nil
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Register creates a new user and returns tokens
|
|
867
|
+
func (u *Usecase) Register(ctx context.Context, req *models.RegisterRequest) (*models.TokenPair, error) {
|
|
868
|
+
// Validate password
|
|
869
|
+
if len(req.Password) < 8 {
|
|
870
|
+
return nil, ErrInvalidPassword
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Check if email exists
|
|
874
|
+
exists, err := u.repo.EmailExists(ctx, req.Email)
|
|
875
|
+
if err != nil {
|
|
876
|
+
u.logger.Error("AuthUsecase", "Email check failed", err)
|
|
877
|
+
return nil, err
|
|
878
|
+
}
|
|
879
|
+
if exists {
|
|
880
|
+
return nil, ErrEmailExists
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Hash password
|
|
884
|
+
hashedPassword, err := authPkg.HashPassword(req.Password)
|
|
885
|
+
if err != nil {
|
|
886
|
+
u.logger.Error("AuthUsecase", "Password hashing failed", err)
|
|
887
|
+
return nil, err
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Create user
|
|
891
|
+
user, err := u.repo.CreateUser(ctx, req.Email, hashedPassword, req.Name, "user")
|
|
892
|
+
if err != nil {
|
|
893
|
+
u.logger.Error("AuthUsecase", "User creation failed", err)
|
|
894
|
+
return nil, err
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Generate tokens
|
|
898
|
+
tokens, err := u.passport.GenerateTokens(user)
|
|
899
|
+
if err != nil {
|
|
900
|
+
u.logger.Error("AuthUsecase", "Token generation failed", err)
|
|
901
|
+
return nil, err
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
u.logger.Info("AuthUsecase", "User registered: "+user.Email)
|
|
905
|
+
return tokens, nil
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// RefreshToken refreshes the access token
|
|
909
|
+
func (u *Usecase) RefreshToken(ctx context.Context, refreshToken string) (*models.TokenPair, error) {
|
|
910
|
+
tokens, err := u.jwtService.RefreshAccessToken(refreshToken)
|
|
911
|
+
if err != nil {
|
|
912
|
+
u.logger.Error("AuthUsecase", "Token refresh failed", err)
|
|
913
|
+
return nil, err
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
u.logger.Info("AuthUsecase", "Token refreshed")
|
|
917
|
+
return tokens, nil
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ValidateToken validates a token and returns the user
|
|
921
|
+
func (u *Usecase) ValidateToken(ctx context.Context, token string) (*models.AuthUser, error) {
|
|
922
|
+
user, err := u.passport.Authenticate(ctx, "jwt", token)
|
|
923
|
+
if err != nil {
|
|
924
|
+
return nil, err
|
|
925
|
+
}
|
|
926
|
+
return user, nil
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// GetJWTService returns the JWT service for middleware use
|
|
930
|
+
func (u *Usecase) GetJWTService() *authPkg.JWTService {
|
|
931
|
+
return u.jwtService
|
|
932
|
+
}
|
|
933
|
+
`;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
module.exports = AuthGenerator;
|