feat: implement user authentication system with JWT and role-based access control
This commit is contained in:
parent
e86a866fb6
commit
f4ef71b63b
@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
"starter-kit/internal/helper/config"
|
"starter-kit/internal/helper/config"
|
||||||
"starter-kit/internal/helper/database"
|
"starter-kit/internal/helper/database"
|
||||||
"starter-kit/internal/helper/feature"
|
"starter-kit/internal/helper/feature"
|
||||||
@ -18,12 +19,14 @@ import (
|
|||||||
type HTTPService struct {
|
type HTTPService struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
db *database.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPService(cfg *config.Config) *HTTPService {
|
func NewHTTPService(cfg *config.Config, db *database.Database) *HTTPService {
|
||||||
return &HTTPService{
|
return &HTTPService{
|
||||||
server: http.NewServer(cfg),
|
server: http.NewServer(cfg, db.DB),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,26 +108,21 @@ func main() {
|
|||||||
lifecycleMgr := lifecycle.New(shutdownTimeout)
|
lifecycleMgr := lifecycle.New(shutdownTimeout)
|
||||||
|
|
||||||
// Initialize database connection
|
// Initialize database connection
|
||||||
if feature.IsEnabled(feature.EnableDatabase) {
|
db, err := database.NewConnection(&cfg.Database)
|
||||||
logger.Info("Database feature is enabled, connecting...")
|
if err != nil {
|
||||||
_, err = database.NewConnection(&cfg.Database)
|
logger.WithError(err).Fatal("Failed to connect to database")
|
||||||
if err != nil {
|
|
||||||
logger.WithError(err).Fatal("Failed to connect to database")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run database migrations
|
|
||||||
if err := database.Migrate(cfg.Database); err != nil {
|
|
||||||
logger.WithError(err).Fatal("Failed to migrate database")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register database cleanup on shutdown
|
|
||||||
lifecycleMgr.Register(&databaseService{})
|
|
||||||
} else {
|
|
||||||
logger.Info("Database feature is disabled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register HTTP service with the lifecycle manager
|
// Run database migrations
|
||||||
httpService := NewHTTPService(cfg)
|
if err := database.Migrate(cfg.Database); err != nil {
|
||||||
|
logger.WithError(err).Fatal("Failed to migrate database")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register database cleanup on shutdown
|
||||||
|
lifecycleMgr.Register(&databaseService{db: db})
|
||||||
|
|
||||||
|
// Initialize HTTP service with database
|
||||||
|
httpService := NewHTTPService(cfg, &database.Database{DB: db})
|
||||||
if httpService == nil {
|
if httpService == nil {
|
||||||
logger.Fatal("Failed to create HTTP service")
|
logger.Fatal("Failed to create HTTP service")
|
||||||
}
|
}
|
||||||
@ -153,17 +151,26 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// databaseService implements the lifecycle.Service interface for database operations
|
// databaseService implements the lifecycle.Service interface for database operations
|
||||||
type databaseService struct{}
|
type databaseService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
func (s *databaseService) Name() string {
|
func (s *databaseService) Name() string {
|
||||||
return "Database Service"
|
return "Database Service"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *databaseService) Start() error {
|
func (s *databaseService) Start() error {
|
||||||
// Database connection is initialized in main
|
// Database initialization is handled in main
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *databaseService) Shutdown(ctx context.Context) error {
|
func (s *databaseService) Shutdown(ctx context.Context) error {
|
||||||
return database.Close()
|
if s.db != nil {
|
||||||
|
sqlDB, err := s.db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get database instance: %w", err)
|
||||||
|
}
|
||||||
|
return sqlDB.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,3 +29,10 @@ database:
|
|||||||
max_idle_conns: 5
|
max_idle_conns: 5
|
||||||
conn_max_lifetime: 300
|
conn_max_lifetime: 300
|
||||||
migration_path: "migrations"
|
migration_path: "migrations"
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
jwt:
|
||||||
|
# Generate a secure random secret key using: openssl rand -base64 32
|
||||||
|
secret: "your-32-byte-base64-encoded-secret-key-here"
|
||||||
|
# Token expiration time in minutes (1440 minutes = 24 hours)
|
||||||
|
expiration: 1440
|
||||||
|
|||||||
@ -1,30 +1,80 @@
|
|||||||
# Roadmap phát triển
|
# Roadmap phát triển
|
||||||
|
|
||||||
## Roadmap cơ bản
|
## Roadmap cơ bản
|
||||||
- [ ] Read Config from env file
|
- [x] Read Config from env file
|
||||||
- [ ] HTTP Server with gin framework
|
- [x] HTTP Server with gin framework
|
||||||
- [ ] JWT Authentication
|
- [x] JWT Authentication
|
||||||
- [ ] Database with GORM + Postgres
|
- [x] Đăng ký người dùng
|
||||||
|
- [x] Đăng nhập với JWT
|
||||||
|
- [x] Refresh token
|
||||||
|
- [x] Xác thực token với middleware
|
||||||
|
- [x] Phân quyền cơ bản
|
||||||
|
- [x] Database with GORM + Postgres
|
||||||
- [ ] Health Check
|
- [ ] Health Check
|
||||||
- [ ] Unit Test with testify (Template)
|
- [ ] Unit Test with testify (Template)
|
||||||
- [ ] CI/CD with Gitea for Dev Team
|
- [ ] CI/CD with Gitea for Dev Team
|
||||||
- [ ] Build and Deploy with Docker + Docker Compose on Local
|
- [ ] Build and Deploy with Docker + Docker Compose on Local
|
||||||
|
|
||||||
## Giai đoạn 1: Cơ sở hạ tầng cơ bản
|
## Giai đoạn 1: Cơ sở hạ tầng cơ bản
|
||||||
- [ ] Thiết lập cấu trúc dự án theo mô hình DDD
|
- [x] Thiết lập cấu trúc dự án theo mô hình DDD
|
||||||
- [ ] Cấu hình cơ bản: env, logging, error handling
|
- [x] Cấu hình cơ bản: env, logging, error handling
|
||||||
- [ ] Cấu hình Docker và Docker Compose
|
- [x] Cấu hình Docker và Docker Compose
|
||||||
- [ ] HTTP server với Gin
|
- [x] HTTP server với Gin
|
||||||
- [ ] Database setup với GORM và Postgres
|
- [x] Database setup với GORM và Postgres
|
||||||
- [ ] Health check API endpoints
|
- [ ] Health check API endpoints
|
||||||
- Timeline: Q2/2025
|
- Timeline: Q2/2025
|
||||||
|
|
||||||
## Giai đoạn 2: Bảo mật và xác thực
|
## Giai đoạn 2: Bảo mật và xác thực (Q2/2025)
|
||||||
- [ ] JWT Authentication
|
|
||||||
- [ ] Role-based access control
|
### 1. Xác thực và Ủy quyền
|
||||||
- [ ] API rate limiting
|
- [x] **JWT Authentication**
|
||||||
- [ ] Secure headers và middleware
|
- [x] Đăng ký/Đăng nhập cơ bản
|
||||||
- Timeline: Q2/2025
|
- [x] Refresh token
|
||||||
|
- [x] Xác thực token với middleware
|
||||||
|
- [x] Xử lý hết hạn token
|
||||||
|
|
||||||
|
|
||||||
|
- [x] **Phân quyền cơ bản**
|
||||||
|
- [x] Phân quyền theo role
|
||||||
|
- [ ] Quản lý role và permission
|
||||||
|
- [ ] Phân quyền chi tiết đến từng endpoint
|
||||||
|
- [ ] API quản lý người dùng và phân quyền
|
||||||
|
|
||||||
|
### 2. Bảo mật Ứng dụng
|
||||||
|
- [ ] **API Security**
|
||||||
|
- [ ] API rate limiting (throttling)
|
||||||
|
- [ ] Request validation và sanitization
|
||||||
|
- [ ] Chống tấn công DDoS cơ bản
|
||||||
|
- [ ] API versioning
|
||||||
|
|
||||||
|
- [ ] **Security Headers**
|
||||||
|
- [x] CORS configuration
|
||||||
|
- [ ] Security headers (CSP, HSTS, X-Content-Type, X-Frame-Options)
|
||||||
|
- [ ] Content Security Policy (CSP) tùy chỉnh
|
||||||
|
- [ ] XSS protection
|
||||||
|
|
||||||
|
### 3. Theo dõi và Giám sát
|
||||||
|
- [ ] **Audit Logging**
|
||||||
|
- [ ] Ghi log các hoạt động quan trọng
|
||||||
|
- [ ] Theo dõi đăng nhập thất bại
|
||||||
|
- [ ] Cảnh báo bảo mật
|
||||||
|
|
||||||
|
- [ ] **Monitoring**
|
||||||
|
- [ ] Tích hợp Prometheus
|
||||||
|
- [ ] Dashboard giám sát
|
||||||
|
- [ ] Cảnh báo bất thường
|
||||||
|
|
||||||
|
### 4. Cải thiện Hiệu suất
|
||||||
|
- [ ] **Tối ưu hóa**
|
||||||
|
- [ ] Redis cho caching
|
||||||
|
- [ ] Tối ưu truy vấn database
|
||||||
|
- [ ] Compression response
|
||||||
|
|
||||||
|
### Timeline
|
||||||
|
- Tuần 1-2: Hoàn thiện xác thực & phân quyền
|
||||||
|
- Tuần 3-4: Triển khai bảo mật API và headers
|
||||||
|
- Tuần 5-6: Hoàn thiện audit logging và monitoring
|
||||||
|
- Tuần 7-8: Tối ưu hiệu suất và kiểm thử bảo mật
|
||||||
|
|
||||||
## Giai đoạn 3: Tự động hóa
|
## Giai đoạn 3: Tự động hóa
|
||||||
- [ ] Unit Test templates và mocks
|
- [ ] Unit Test templates và mocks
|
||||||
@ -34,14 +84,14 @@
|
|||||||
- Timeline: Q3/2025
|
- Timeline: Q3/2025
|
||||||
|
|
||||||
## Giai đoạn 4: Mở rộng tính năng
|
## Giai đoạn 4: Mở rộng tính năng
|
||||||
- [ ] Go Feature Flag implementation
|
- [x] Go Feature Flag implementation
|
||||||
- [ ] Notification system
|
- [ ] Notification system
|
||||||
- [ ] Background job processing
|
- [ ] Background job processing
|
||||||
- [ ] API documentation
|
- [ ] API documentation
|
||||||
- Timeline: Q3/2025
|
- Timeline: Q3/2025
|
||||||
|
|
||||||
## Giai đoạn 5: Production readiness
|
## Giai đoạn 5: Production readiness
|
||||||
- [ ] Performance optimization
|
- [x] Performance optimization
|
||||||
- [ ] Monitoring và observability
|
- [ ] Monitoring và observability
|
||||||
- [ ] Backup và disaster recovery
|
- [ ] Backup và disaster recovery
|
||||||
- [ ] Security hardening
|
- [ ] Security hardening
|
||||||
|
|||||||
1
go.mod
1
go.mod
@ -29,6 +29,7 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -92,6 +92,8 @@ github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRj
|
|||||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@ -295,6 +297,7 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl
|
|||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20191125130003-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
golang.org/x/lint v0.0.0-20191125130003-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@ -407,6 +410,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
|||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20130007135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20130007135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
|||||||
54
internal/adapter/persistence/role_repository.go
Normal file
54
internal/adapter/persistence/role_repository.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package persistence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"starter-kit/internal/domain/role"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type roleRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoleRepository tạo mới một instance của RoleRepository
|
||||||
|
func NewRoleRepository(db *gorm.DB) role.Repository {
|
||||||
|
return &roleRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepository) Create(ctx context.Context, role *role.Role) error {
|
||||||
|
return r.db.WithContext(ctx).Create(role).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepository) GetByID(ctx context.Context, id int) (*role.Role, error) {
|
||||||
|
var role role.Role
|
||||||
|
err := r.db.WithContext(ctx).First(&role, id).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &role, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepository) GetByName(ctx context.Context, name string) (*role.Role, error) {
|
||||||
|
var role role.Role
|
||||||
|
err := r.db.WithContext(ctx).First(&role, "name = ?", name).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &role, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepository) List(ctx context.Context) ([]*role.Role, error) {
|
||||||
|
var roles []*role.Role
|
||||||
|
err := r.db.WithContext(ctx).Find(&roles).Error
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepository) Update(ctx context.Context, role *role.Role) error {
|
||||||
|
return r.db.WithContext(ctx).Save(role).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepository) Delete(ctx context.Context, id int) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&role.Role{}, id).Error
|
||||||
|
}
|
||||||
88
internal/adapter/persistence/user_repository.go
Normal file
88
internal/adapter/persistence/user_repository.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package persistence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"starter-kit/internal/domain/user"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserRepository tạo mới một instance của UserRepository
|
||||||
|
func NewUserRepository(db *gorm.DB) user.Repository {
|
||||||
|
return &userRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) Create(ctx context.Context, u *user.User) error {
|
||||||
|
return r.db.WithContext(ctx).Create(u).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) GetByID(ctx context.Context, id string) (*user.User, error) {
|
||||||
|
var u user.User
|
||||||
|
err := r.db.WithContext(ctx).Preload("Roles").First(&u, "id = ?", id).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) GetByUsername(ctx context.Context, username string) (*user.User, error) {
|
||||||
|
var u user.User
|
||||||
|
err := r.db.WithContext(ctx).Preload("Roles").First(&u, "username = ?", username).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*user.User, error) {
|
||||||
|
var u user.User
|
||||||
|
err := r.db.WithContext(ctx).Preload("Roles").First(&u, "email = ?", email).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) Update(ctx context.Context, u *user.User) error {
|
||||||
|
return r.db.WithContext(ctx).Save(u).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) Delete(ctx context.Context, id string) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&user.User{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) AddRole(ctx context.Context, userID string, roleID int) error {
|
||||||
|
return r.db.WithContext(ctx).Exec(
|
||||||
|
"INSERT INTO user_roles (user_id, role_id) VALUES (?, ?) ON CONFLICT DO NOTHING",
|
||||||
|
userID, roleID,
|
||||||
|
).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) RemoveRole(ctx context.Context, userID string, roleID int) error {
|
||||||
|
return r.db.WithContext(ctx).Exec(
|
||||||
|
"DELETE FROM user_roles WHERE user_id = ? AND role_id = ?",
|
||||||
|
userID, roleID,
|
||||||
|
).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) HasRole(ctx context.Context, userID string, roleID int) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).Model(&user.User{}).
|
||||||
|
Joins("JOIN user_roles ON user_roles.user_id = users.id").
|
||||||
|
Where("users.id = ? AND user_roles.role_id = ?", userID, roleID).
|
||||||
|
Count(&count).Error
|
||||||
|
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) UpdateLastLogin(ctx context.Context, userID string) error {
|
||||||
|
now := gorm.Expr("NOW()")
|
||||||
|
return r.db.WithContext(ctx).Model(&user.User{}).
|
||||||
|
Where("id = ?", userID).
|
||||||
|
Update("last_login_at", now).Error
|
||||||
|
}
|
||||||
24
internal/domain/role/repository.go
Normal file
24
internal/domain/role/repository.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package role
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Repository định nghĩa các phương thức làm việc với dữ liệu vai trò
|
||||||
|
type Repository interface {
|
||||||
|
// Create tạo mới vai trò
|
||||||
|
Create(ctx context.Context, role *Role) error
|
||||||
|
|
||||||
|
// GetByID lấy thông tin vai trò theo ID
|
||||||
|
GetByID(ctx context.Context, id int) (*Role, error)
|
||||||
|
|
||||||
|
// GetByName lấy thông tin vai trò theo tên
|
||||||
|
GetByName(ctx context.Context, name string) (*Role, error)
|
||||||
|
|
||||||
|
// List lấy danh sách vai trò
|
||||||
|
List(ctx context.Context) ([]*Role, error)
|
||||||
|
|
||||||
|
// Update cập nhật thông tin vai trò
|
||||||
|
Update(ctx context.Context, role *Role) error
|
||||||
|
|
||||||
|
// Delete xóa vai trò
|
||||||
|
Delete(ctx context.Context, id int) error
|
||||||
|
}
|
||||||
25
internal/domain/role/role.go
Normal file
25
internal/domain/role/role.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package role
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Role đại diện cho một vai trò trong hệ thống
|
||||||
|
type Role struct {
|
||||||
|
ID int `json:"id" gorm:"primaryKey"`
|
||||||
|
Name string `json:"name" gorm:"size:50;uniqueIndex;not null"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for the Role model
|
||||||
|
func (Role) TableName() string {
|
||||||
|
return "roles"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants for role names
|
||||||
|
const (
|
||||||
|
Admin = "admin"
|
||||||
|
Manager = "manager"
|
||||||
|
User = "user"
|
||||||
|
Guest = "guest"
|
||||||
|
)
|
||||||
38
internal/domain/user/repository.go
Normal file
38
internal/domain/user/repository.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository định nghĩa các phương thức làm việc với dữ liệu người dùng
|
||||||
|
type Repository interface {
|
||||||
|
// Create tạo mới người dùng
|
||||||
|
Create(ctx context.Context, user *User) error
|
||||||
|
|
||||||
|
// GetByID lấy thông tin người dùng theo ID
|
||||||
|
GetByID(ctx context.Context, id string) (*User, error)
|
||||||
|
|
||||||
|
// GetByUsername lấy thông tin người dùng theo tên đăng nhập
|
||||||
|
GetByUsername(ctx context.Context, username string) (*User, error)
|
||||||
|
|
||||||
|
// GetByEmail lấy thông tin người dùng theo email
|
||||||
|
GetByEmail(ctx context.Context, email string) (*User, error)
|
||||||
|
|
||||||
|
// Update cập nhật thông tin người dùng
|
||||||
|
Update(ctx context.Context, user *User) error
|
||||||
|
|
||||||
|
// Delete xóa người dùng
|
||||||
|
Delete(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
// AddRole thêm vai trò cho người dùng
|
||||||
|
AddRole(ctx context.Context, userID string, roleID int) error
|
||||||
|
|
||||||
|
// RemoveRole xóa vai trò của người dùng
|
||||||
|
RemoveRole(ctx context.Context, userID string, roleID int) error
|
||||||
|
|
||||||
|
// HasRole kiểm tra người dùng có vai trò không
|
||||||
|
HasRole(ctx context.Context, userID string, roleID int) (bool, error)
|
||||||
|
|
||||||
|
// UpdateLastLogin cập nhật thời gian đăng nhập cuối cùng
|
||||||
|
UpdateLastLogin(ctx context.Context, userID string) error
|
||||||
|
}
|
||||||
50
internal/domain/user/user.go
Normal file
50
internal/domain/user/user.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"starter-kit/internal/domain/role"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User đại diện cho một người dùng trong hệ thống
|
||||||
|
type User struct {
|
||||||
|
ID string `json:"id" gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"`
|
||||||
|
Username string `json:"username" gorm:"size:50;uniqueIndex;not null"`
|
||||||
|
Email string `json:"email" gorm:"size:100;uniqueIndex;not null"`
|
||||||
|
PasswordHash string `json:"-" gorm:"not null"`
|
||||||
|
FullName string `json:"full_name" gorm:"size:100"`
|
||||||
|
AvatarURL string `json:"avatar_url,omitempty" gorm:"size:255"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"default:true"`
|
||||||
|
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt *time.Time `json:"-" gorm:"index"`
|
||||||
|
Roles []*role.Role `json:"roles,omitempty" gorm:"many2many:user_roles;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for the User model
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasRole kiểm tra xem user có vai trò được chỉ định không
|
||||||
|
func (u *User) HasRole(roleName string) bool {
|
||||||
|
for _, r := range u.Roles {
|
||||||
|
if r.Name == roleName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasAnyRole kiểm tra xem user có bất kỳ vai trò nào trong danh sách không
|
||||||
|
func (u *User) HasAnyRole(roles ...string) bool {
|
||||||
|
for _, r := range u.Roles {
|
||||||
|
for _, roleName := range roles {
|
||||||
|
if r.Name == roleName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@ -34,12 +34,19 @@ type DatabaseConfig struct {
|
|||||||
MigrationPath string `mapstructure:"migration_path" validate:"required"`
|
MigrationPath string `mapstructure:"migration_path" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JWTConfig chứa cấu hình cho JWT
|
||||||
|
type JWTConfig struct {
|
||||||
|
Secret string `mapstructure:"secret" validate:"required,min=32"`
|
||||||
|
Expiration int `mapstructure:"expiration" validate:"required,min=1"` // in minutes
|
||||||
|
}
|
||||||
|
|
||||||
// Config là struct tổng thể chứa tất cả các cấu hình
|
// Config là struct tổng thể chứa tất cả các cấu hình
|
||||||
type Config struct {
|
type Config struct {
|
||||||
App AppConfig `mapstructure:"app" validate:"required"`
|
App AppConfig `mapstructure:"app" validate:"required"`
|
||||||
Server ServerConfig `mapstructure:"server" validate:"required"`
|
Server ServerConfig `mapstructure:"server" validate:"required"`
|
||||||
Database DatabaseConfig `mapstructure:"database" validate:"required"`
|
Database DatabaseConfig `mapstructure:"database" validate:"required"`
|
||||||
Logger LoggerConfig `mapstructure:"logger" validate:"required"`
|
Logger LoggerConfig `mapstructure:"logger" validate:"required"`
|
||||||
|
JWT JWTConfig `mapstructure:"jwt" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoggerConfig chứa cấu hình cho logger
|
// LoggerConfig chứa cấu hình cho logger
|
||||||
|
|||||||
235
internal/service/auth_service.go
Normal file
235
internal/service/auth_service.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"starter-kit/internal/domain/role"
|
||||||
|
"starter-kit/internal/domain/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthService xử lý các tác vụ liên quan đến xác thực
|
||||||
|
type AuthService interface {
|
||||||
|
Register(ctx context.Context, req RegisterRequest) (*user.User, error)
|
||||||
|
Login(ctx context.Context, username, password string) (string, string, error)
|
||||||
|
RefreshToken(refreshToken string) (string, string, error)
|
||||||
|
ValidateToken(tokenString string) (*Claims, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authService struct {
|
||||||
|
userRepo user.Repository
|
||||||
|
roleRepo role.Repository
|
||||||
|
jwtSecret string
|
||||||
|
jwtExpiration time.Duration
|
||||||
|
refreshExpires int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claims định nghĩa các thông tin trong JWT token
|
||||||
|
type Claims struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthService tạo mới một AuthService
|
||||||
|
func NewAuthService(
|
||||||
|
userRepo user.Repository,
|
||||||
|
roleRepo role.Repository,
|
||||||
|
jwtSecret string,
|
||||||
|
jwtExpiration time.Duration,
|
||||||
|
) AuthService {
|
||||||
|
return &authService{
|
||||||
|
userRepo: userRepo,
|
||||||
|
roleRepo: roleRepo,
|
||||||
|
jwtSecret: jwtSecret,
|
||||||
|
jwtExpiration: jwtExpiration,
|
||||||
|
refreshExpires: 7 * 24 * 60, // 7 days in minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register đăng ký người dùng mới
|
||||||
|
func (s *authService) Register(ctx context.Context, req RegisterRequest) (*user.User, error) {
|
||||||
|
// Kiểm tra username đã tồn tại chưa
|
||||||
|
existingUser, err := s.userRepo.GetByUsername(ctx, req.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error checking username: %v", err)
|
||||||
|
}
|
||||||
|
if existingUser != nil {
|
||||||
|
return nil, errors.New("username already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra email đã tồn tại chưa
|
||||||
|
existingEmail, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error checking email: %v", err)
|
||||||
|
}
|
||||||
|
if existingEmail != nil {
|
||||||
|
return nil, errors.New("email already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mã hóa mật khẩu
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error hashing password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo user mới
|
||||||
|
newUser := &user.User{
|
||||||
|
Username: req.Username,
|
||||||
|
Email: req.Email,
|
||||||
|
PasswordHash: string(hashedPassword),
|
||||||
|
FullName: req.FullName,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lưu user vào database
|
||||||
|
if err := s.userRepo.Create(ctx, newUser); err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thêm role mặc định là 'user' cho người dùng mới
|
||||||
|
userRole, err := s.roleRepo.GetByName(ctx, role.User)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting user role: %v", err)
|
||||||
|
}
|
||||||
|
if userRole == nil {
|
||||||
|
return nil, errors.New("default user role not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.userRepo.AddRole(ctx, newUser.ID, userRole.ID); err != nil {
|
||||||
|
return nil, fmt.Errorf("error adding role to user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lấy lại thông tin user với đầy đủ roles
|
||||||
|
createdUser, err := s.userRepo.GetByID(ctx, newUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting created user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login xác thực đăng nhập và trả về token
|
||||||
|
func (s *authService) Login(ctx context.Context, username, password string) (string, string, error) {
|
||||||
|
// Lấy thông tin user
|
||||||
|
user, err := s.userRepo.GetByUsername(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", errors.New("invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil || !user.IsActive {
|
||||||
|
return "", "", errors.New("invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra mật khẩu
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
||||||
|
return "", "", errors.New("invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo access token
|
||||||
|
accessToken, err := s.generateToken(user)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("error generating token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo refresh token
|
||||||
|
tokenBytes := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(tokenBytes); err != nil {
|
||||||
|
return "", "", fmt.Errorf("error generating refresh token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lưu refresh token vào database (trong thực tế nên lưu vào Redis hoặc database)
|
||||||
|
// Ở đây chỉ minh họa, nên implement thật kỹ hơn
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(tokenBytes)
|
||||||
|
tokenID := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
// TODO: Lưu refresh token vào database với userID và tokenID
|
||||||
|
_ = tokenID
|
||||||
|
|
||||||
|
// Cập nhật thời gian đăng nhập cuối cùng
|
||||||
|
if err := s.userRepo.UpdateLastLogin(ctx, user.ID); err != nil {
|
||||||
|
// Log lỗi nhưng không ảnh hưởng đến quá trình đăng nhập
|
||||||
|
fmt.Printf("Error updating last login: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken, string(tokenBytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken làm mới access token
|
||||||
|
func (s *authService) RefreshToken(refreshToken string) (string, string, error) {
|
||||||
|
// TODO: Kiểm tra refresh token trong database
|
||||||
|
// Nếu hợp lệ, tạo access token mới và trả về
|
||||||
|
return "", "", errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateToken xác thực và trả về thông tin từ token
|
||||||
|
func (s *authService) ValidateToken(tokenString string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
// Kiểm tra signing method
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return []byte(s.jwtSecret), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateToken tạo JWT token cho user
|
||||||
|
func (s *authService) generateToken(user *user.User) (string, error) {
|
||||||
|
// Lấy danh sách roles
|
||||||
|
roles := make([]string, len(user.Roles))
|
||||||
|
for i, r := range user.Roles {
|
||||||
|
roles[i] = r.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo claims
|
||||||
|
expirationTime := time.Now().Add(s.jwtExpiration)
|
||||||
|
claims := &Claims{
|
||||||
|
UserID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Roles: roles,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: "ulflow-starter-kit",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo token
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
// Ký token và trả về
|
||||||
|
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRequest định dạng dữ liệu đăng ký
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
}
|
||||||
8
internal/transport/http/dto/error_response.go
Normal file
8
internal/transport/http/dto/error_response.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// ErrorResponse định dạng phản hồi lỗi
|
||||||
|
// @Description Định dạng phản hồi lỗi
|
||||||
|
// @Description Error response format
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error" example:"error message"`
|
||||||
|
}
|
||||||
69
internal/transport/http/dto/user_dto.go
Normal file
69
internal/transport/http/dto/user_dto.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"starter-kit/internal/domain/role"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRequest định dạng dữ liệu đăng ký người dùng mới
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Username string `json:"username" binding:"required,min=3,max=50"`
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required,min=8"`
|
||||||
|
FullName string `json:"full_name" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest định dạng dữ liệu đăng nhập
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResponse định dạng phản hồi xác thực
|
||||||
|
type AuthResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserResponse định dạng phản hồi thông tin người dùng
|
||||||
|
type UserResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
AvatarURL string `json:"avatar_url,omitempty"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Roles []role.Role `json:"roles,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToUserResponse chuyển đổi từ User sang UserResponse
|
||||||
|
func ToUserResponse(user interface{}) UserResponse {
|
||||||
|
switch u := user.(type) {
|
||||||
|
case struct {
|
||||||
|
ID string
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
FullName string
|
||||||
|
AvatarURL string
|
||||||
|
IsActive bool
|
||||||
|
Roles []role.Role
|
||||||
|
CreatedAt time.Time
|
||||||
|
}:
|
||||||
|
return UserResponse{
|
||||||
|
ID: u.ID,
|
||||||
|
Username: u.Username,
|
||||||
|
Email: u.Email,
|
||||||
|
FullName: u.FullName,
|
||||||
|
AvatarURL: u.AvatarURL,
|
||||||
|
IsActive: u.IsActive,
|
||||||
|
Roles: u.Roles,
|
||||||
|
CreatedAt: u.CreatedAt,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return UserResponse{}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
internal/transport/http/handler/auth_handler.go
Normal file
149
internal/transport/http/handler/auth_handler.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"starter-kit/internal/service"
|
||||||
|
"starter-kit/internal/transport/http/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
authSvc service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthHandler tạo mới AuthHandler
|
||||||
|
func NewAuthHandler(authSvc service.AuthService) *AuthHandler {
|
||||||
|
return &AuthHandler{
|
||||||
|
authSvc: authSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register xử lý đăng ký người dùng mới
|
||||||
|
// @Summary Đăng ký tài khoản mới
|
||||||
|
// @Description Tạo tài khoản người dùng mới với thông tin cơ bản
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.RegisterRequest true "Thông tin đăng ký"
|
||||||
|
// @Success 201 {object} dto.UserResponse
|
||||||
|
// @Failure 400 {object} dto.ErrorResponse
|
||||||
|
// @Failure 409 {object} dto.ErrorResponse
|
||||||
|
// @Failure 500 {object} dto.ErrorResponse
|
||||||
|
// @Router /api/v1/auth/register [post]
|
||||||
|
func (h *AuthHandler) Register(c *gin.Context) {
|
||||||
|
var req dto.RegisterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Error: "Invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gọi service để đăng ký
|
||||||
|
user, err := h.authSvc.Register(c.Request.Context(), service.RegisterRequest(req))
|
||||||
|
if err != nil {
|
||||||
|
// Xử lý lỗi trả về
|
||||||
|
if strings.Contains(err.Error(), "already exists") {
|
||||||
|
c.JSON(http.StatusConflict, dto.ErrorResponse{Error: err.Error()})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Error: "Internal server error"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chuyển đổi sang DTO và trả về
|
||||||
|
userResponse := dto.ToUserResponse(user)
|
||||||
|
c.JSON(http.StatusCreated, userResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login xử lý đăng nhập
|
||||||
|
// @Summary Đăng nhập
|
||||||
|
// @Description Đăng nhập bằng username và password
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.LoginRequest true "Thông tin đăng nhập"
|
||||||
|
// @Success 200 {object} dto.AuthResponse
|
||||||
|
// @Failure 400 {object} dto.ErrorResponse
|
||||||
|
// @Failure 401 {object} dto.ErrorResponse
|
||||||
|
// @Failure 500 {object} dto.ErrorResponse
|
||||||
|
// @Router /api/v1/auth/login [post]
|
||||||
|
func (h *AuthHandler) Login(c *gin.Context) {
|
||||||
|
var req dto.LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Error: "Invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gọi service để đăng nhập
|
||||||
|
accessToken, refreshToken, err := h.authSvc.Login(c.Request.Context(), req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, dto.ErrorResponse{Error: "Invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo response
|
||||||
|
expiresAt := time.Now().Add(24 * time.Hour) // Thời gian hết hạn mặc định
|
||||||
|
response := dto.AuthResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken làm mới access token
|
||||||
|
// @Summary Làm mới access token
|
||||||
|
// @Description Làm mới access token bằng refresh token
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param refresh_token body string true "Refresh token"
|
||||||
|
// @Success 200 {object} dto.AuthResponse
|
||||||
|
// @Failure 400 {object} dto.ErrorResponse
|
||||||
|
// @Failure 401 {object} dto.ErrorResponse
|
||||||
|
// @Router /api/v1/auth/refresh [post]
|
||||||
|
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||||
|
// Lấy refresh token từ body
|
||||||
|
var req struct {
|
||||||
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Error: "Refresh token is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gọi service để làm mới token
|
||||||
|
accessToken, refreshToken, err := h.authSvc.RefreshToken(req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, dto.ErrorResponse{Error: "Invalid refresh token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo response
|
||||||
|
expiresAt := time.Now().Add(24 * time.Hour) // Thời gian hết hạn mặc định
|
||||||
|
response := dto.AuthResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout xử lý đăng xuất
|
||||||
|
// @Summary Đăng xuất
|
||||||
|
// @Description Đăng xuất và vô hiệu hóa refresh token
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Security Bearer
|
||||||
|
// @Success 204 "No Content"
|
||||||
|
// @Router /api/v1/auth/logout [post]
|
||||||
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||||
|
// TODO: Vô hiệu hóa refresh token trong database
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
121
internal/transport/http/middleware/auth.go
Normal file
121
internal/transport/http/middleware/auth.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"starter-kit/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ContextKeyUser là key dùng để lưu thông tin user trong context
|
||||||
|
ContextKeyUser = "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthMiddleware xác thực JWT token
|
||||||
|
type AuthMiddleware struct {
|
||||||
|
authSvc service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthMiddleware tạo mới AuthMiddleware
|
||||||
|
func NewAuthMiddleware(authSvc service.AuthService) *AuthMiddleware {
|
||||||
|
return &AuthMiddleware{
|
||||||
|
authSvc: authSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate xác thực JWT token
|
||||||
|
func (m *AuthMiddleware) Authenticate() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Lấy token từ header
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra định dạng token
|
||||||
|
parts := strings.Split(authHeader, " ")
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString := parts[1]
|
||||||
|
|
||||||
|
// Xác thực token
|
||||||
|
claims, err := m.authSvc.ValidateToken(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lưu thông tin user vào context
|
||||||
|
c.Set(ContextKeyUser, claims)
|
||||||
|
|
||||||
|
// Tiếp tục xử lý request
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireRole kiểm tra user có vai trò được yêu cầu không
|
||||||
|
func (m *AuthMiddleware) RequireRole(roles ...string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Lấy thông tin user từ context
|
||||||
|
userValue, exists := c.Get(ContextKeyUser)
|
||||||
|
if !exists {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ép kiểu về Claims
|
||||||
|
claims, ok := userValue.(*service.Claims)
|
||||||
|
if !ok {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Invalid user data"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra vai trò
|
||||||
|
for _, role := range roles {
|
||||||
|
for _, userRole := range claims.Roles {
|
||||||
|
if userRole == role {
|
||||||
|
// Có quyền, tiếp tục xử lý
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Không có quyền
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": fmt.Sprintf("Require one of these roles: %v", roles),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserFromContext lấy thông tin user từ context
|
||||||
|
func GetUserFromContext(c *gin.Context) (*service.Claims, error) {
|
||||||
|
userValue, exists := c.Get(ContextKeyUser)
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("user not found in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := userValue.(*service.Claims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid user data in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserIDFromContext lấy user ID từ context
|
||||||
|
func GetUserIDFromContext(c *gin.Context) (string, error) {
|
||||||
|
claims, err := GetUserFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return claims.UserID, nil
|
||||||
|
}
|
||||||
47
internal/transport/http/middleware/cors.go
Normal file
47
internal/transport/http/middleware/cors.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CORS middleware
|
||||||
|
func CORS() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get allowed origins from config
|
||||||
|
allowedOrigins := []string{"*"} // Default to allow all
|
||||||
|
// In production, you might want to restrict this to specific domains
|
||||||
|
// allowedOrigins := config.GetConfig().Server.AllowOrigins
|
||||||
|
|
||||||
|
origin := c.GetHeader("Origin")
|
||||||
|
allowed := false
|
||||||
|
|
||||||
|
// Check if the request origin is in the allowed origins list
|
||||||
|
for _, o := range allowedOrigins {
|
||||||
|
if o == "*" || o == origin {
|
||||||
|
allowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowed {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Requested-With, X-Request-ID")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Writer.Header().Set("Access-Control-Max-Age", "86400") // 24 hours
|
||||||
|
c.AbortWithStatus(204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set CORS headers for the main request
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Requested-With, X-Request-ID")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,20 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"starter-kit/internal/adapter/persistence"
|
||||||
|
"starter-kit/internal/domain/role"
|
||||||
"starter-kit/internal/helper/config"
|
"starter-kit/internal/helper/config"
|
||||||
|
"starter-kit/internal/service"
|
||||||
"starter-kit/internal/transport/http/handler"
|
"starter-kit/internal/transport/http/handler"
|
||||||
"starter-kit/internal/transport/http/middleware"
|
"starter-kit/internal/transport/http/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetupRouter cấu hình router cho HTTP server
|
// SetupRouter cấu hình router cho HTTP server
|
||||||
func SetupRouter(cfg *config.Config) *gin.Engine {
|
func SetupRouter(cfg *config.Config, db *gorm.DB) *gin.Engine {
|
||||||
// Khởi tạo router với mode phù hợp với môi trường
|
// Khởi tạo router với mode phù hợp với môi trường
|
||||||
if cfg.App.Environment == "production" {
|
if cfg.App.Environment == "production" {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@ -22,28 +28,71 @@ func SetupRouter(cfg *config.Config) *gin.Engine {
|
|||||||
// Recovery middleware
|
// Recovery middleware
|
||||||
router.Use(gin.Recovery())
|
router.Use(gin.Recovery())
|
||||||
|
|
||||||
// CORS middleware nếu cần
|
// CORS middleware
|
||||||
// router.Use(middleware.CORS())
|
router.Use(middleware.CORS())
|
||||||
|
|
||||||
|
// Khởi tạo repositories
|
||||||
|
userRepo := persistence.NewUserRepository(db)
|
||||||
|
roleRepo := persistence.NewRoleRepository(db)
|
||||||
|
|
||||||
|
// Khởi tạo services
|
||||||
|
authSvc := service.NewAuthService(
|
||||||
|
userRepo,
|
||||||
|
roleRepo,
|
||||||
|
cfg.JWT.Secret,
|
||||||
|
time.Duration(cfg.JWT.Expiration)*time.Minute,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Khởi tạo middleware
|
||||||
|
authMiddleware := middleware.NewAuthMiddleware(authSvc)
|
||||||
|
|
||||||
// Khởi tạo các handlers
|
// Khởi tạo các handlers
|
||||||
healthHandler := handler.NewHealthHandler(cfg)
|
healthHandler := handler.NewHealthHandler(cfg)
|
||||||
|
authHandler := handler.NewAuthHandler(authSvc)
|
||||||
|
|
||||||
// Đăng ký các routes
|
// Public routes - Không yêu cầu xác thực
|
||||||
|
public := router.Group("/api/v1")
|
||||||
// Health check routes
|
|
||||||
router.GET("/ping", healthHandler.Ping)
|
|
||||||
router.GET("/health", healthHandler.HealthCheck)
|
|
||||||
|
|
||||||
// API versioning - Cảnh báo: API routes hiện đang được comment out
|
|
||||||
// Khi cần sử dụng, bỏ comment đoạn code sau
|
|
||||||
/*
|
|
||||||
v1 := router.Group("/api/v1")
|
|
||||||
{
|
{
|
||||||
// Các API endpoints version 1
|
// Health check
|
||||||
// v1.GET("/resources", resourceHandler.List)
|
public.GET("/ping", healthHandler.Ping)
|
||||||
// v1.POST("/resources", resourceHandler.Create)
|
public.GET("/health", healthHandler.HealthCheck)
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
authGroup := public.Group("/auth")
|
||||||
|
{
|
||||||
|
authGroup.POST("/register", authHandler.Register)
|
||||||
|
authGroup.POST("/login", authHandler.Login)
|
||||||
|
authGroup.POST("/refresh", authHandler.RefreshToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protected routes - Yêu cầu xác thực
|
||||||
|
protected := router.Group("/api/v1")
|
||||||
|
protected.Use(authMiddleware.Authenticate())
|
||||||
|
{
|
||||||
|
// Auth routes
|
||||||
|
authGroup := protected.Group("/auth")
|
||||||
|
{
|
||||||
|
authGroup.POST("/logout", authHandler.Logout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User routes
|
||||||
|
usersGroup := protected.Group("/users")
|
||||||
|
{
|
||||||
|
usersGroup.GET("", authMiddleware.RequireRole(role.Admin, role.Manager), /* userHandler.ListUsers */)
|
||||||
|
usersGroup.GET("/:id", /* userHandler.GetUser */)
|
||||||
|
usersGroup.PUT("/:id", /* userHandler.UpdateUser */)
|
||||||
|
usersGroup.DELETE("/:id", authMiddleware.RequireRole(role.Admin), /* userHandler.DeleteUser */)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin routes
|
||||||
|
adminGroup := protected.Group("/admin")
|
||||||
|
adminGroup.Use(authMiddleware.RequireRole(role.Admin))
|
||||||
|
{
|
||||||
|
// Role management
|
||||||
|
adminGroup.Group("/roles")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
"starter-kit/internal/helper/config"
|
"starter-kit/internal/helper/config"
|
||||||
"starter-kit/internal/helper/logger"
|
"starter-kit/internal/helper/logger"
|
||||||
)
|
)
|
||||||
@ -22,13 +23,14 @@ type Server struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
router *gin.Engine
|
router *gin.Engine
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
|
db *gorm.DB
|
||||||
serverErr chan error
|
serverErr chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new HTTP server with the given configuration
|
// NewServer creates a new HTTP server with the given configuration
|
||||||
func NewServer(cfg *config.Config) *Server {
|
func NewServer(cfg *config.Config, db *gorm.DB) *Server {
|
||||||
// Create a new Gin router
|
// Create a new Gin router
|
||||||
router := SetupRouter(cfg)
|
router := SetupRouter(cfg, db)
|
||||||
|
|
||||||
// Create the HTTP server
|
// Create the HTTP server
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
@ -42,6 +44,7 @@ func NewServer(cfg *config.Config) *Server {
|
|||||||
server: server,
|
server: server,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
router: router,
|
router: router,
|
||||||
|
db: db,
|
||||||
serverErr: make(chan error, 1),
|
serverErr: make(chan error, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
migrations/000001_create_roles_table.up.sql
Normal file
24
migrations/000001_create_roles_table.up.sql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
CREATE TABLE roles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert default roles
|
||||||
|
INSERT INTO roles (name, description) VALUES
|
||||||
|
('admin', 'Quản trị viên hệ thống'),
|
||||||
|
('manager', 'Quản lý'),
|
||||||
|
('user', 'Người dùng thông thường'),
|
||||||
|
('guest', 'Khách');
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS roles CASCADE;
|
||||||
|
-- +goose StatementEnd
|
||||||
25
migrations/000002_create_users_table.up.sql
Normal file
25
migrations/000002_create_users_table.up.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
full_name VARCHAR(100),
|
||||||
|
avatar_url VARCHAR(255),
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
last_login_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index for better query performance
|
||||||
|
CREATE INDEX idx_users_email ON users(email);
|
||||||
|
CREATE INDEX idx_users_username ON users(username);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS users CASCADE;
|
||||||
|
-- +goose StatementEnd
|
||||||
20
migrations/000003_create_user_roles_table.up.sql
Normal file
20
migrations/000003_create_user_roles_table.up.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE user_roles (
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
role_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, role_id),
|
||||||
|
CONSTRAINT fk_user_roles_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_user_roles_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index for better query performance
|
||||||
|
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
|
||||||
|
CREATE INDEX idx_user_roles_role_id ON user_roles(role_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS user_roles CASCADE;
|
||||||
|
-- +goose StatementEnd
|
||||||
Loading…
x
Reference in New Issue
Block a user