Merge pull request 'chore/update-config-file' (#2) from chore/update-config-file into main

Reviewed-on: #2
This commit is contained in:
ulflow_phattt2901 2025-05-29 05:21:22 +00:00
commit 7d40d3b456
17 changed files with 767 additions and 209 deletions

32
.env.example Normal file
View File

@ -0,0 +1,32 @@
# App Configuration
APP_NAME="ULFlow Starter Kit"
APP_VERSION="0.1.0"
APP_ENVIRONMENT="development"
APP_TIMEZONE="Asia/Ho_Chi_Minh"
# Logger Configuration
LOG_LEVEL="info" # debug, info, warn, error
# Server Configuration
SERVER_HOST="0.0.0.0"
SERVER_PORT=3000
SERVER_READ_TIMEOUT=15
SERVER_WRITE_TIMEOUT=15
SERVER_SHUTDOWN_TIMEOUT=30
# Database Configuration
DB_DRIVER="postgres"
DB_HOST="localhost"
DB_PORT=5432
DB_USERNAME="postgres"
DB_PASSWORD="your_password_here"
DB_NAME="ulflow"
DB_SSLMODE="disable"
# JWT Configuration
JWT_SECRET="your-32-byte-base64-encoded-secret-key-here"
JWT_ACCESS_TOKEN_EXPIRE=15 # in minutes
JWT_REFRESH_TOKEN_EXPIRE=10080 # in minutes (7 days)
JWT_ALGORITHM="HS256"
JWT_ISSUER="ulflow-starter-kit"
JWT_AUDIENCE="ulflow-web"

View File

@ -1,5 +1,5 @@
{
"enable_database": {
"enabled": false
"enabled": true
}
}

View File

@ -99,12 +99,12 @@ jobs:
- name: Build
run: |
APP_VERSION="dev-${{ gitea.sha }}"
go build -v -ldflags="-s -w -X main.version=${APP_VERSION}" -o ./bin/api ./cmd/api
go build -v -ldflags="-s -w -X main.version=${APP_VERSION}" -o ./bin/app ./cmd/app
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: api-binary
name: app-binary
path: ./bin/api
- name: Notify on success

View File

@ -29,3 +29,51 @@ database:
max_idle_conns: 5
conn_max_lifetime: 300
migration_path: "migrations"
<<<<<<< Updated upstream
=======
# JWT Configuration
jwt:
# Generate a secure random secret key using: openssl rand -base64 32
secret: "ulflow2121_this_is_a_secure_key_for_jwt_signing"
# Access Token expiration time in minutes (15 minutes)
access_token_expire: 15
# Refresh Token expiration time in minutes (7 days = 10080 minutes)
refresh_token_expire: 10080
# Algorithm for JWT signing (HS256, HS384, HS512, RS256, etc.)
algorithm: "HS256"
# Issuer for JWT tokens
issuer: "ulflow-starter-kit"
# Audience for JWT tokens
audience: ["ulflow-web"]
# Security configurations
security:
# Rate limiting for authentication endpoints (requests per minute)
rate_limit:
login: 5
register: 3
refresh: 10
# Password policy
password:
min_length: 8
require_upper: true
require_lower: true
require_number: true
require_special: true
# Cookie settings
cookie:
secure: true
http_only: true
same_site: "Lax" # or "Strict" for more security
domain: "" # Set your domain in production
path: "/"
# CORS settings
cors:
allowed_origins: ["*"] # Restrict in production
allowed_methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
allowed_headers: ["Origin", "Content-Type", "Accept", "Authorization"]
exposed_headers: ["Content-Length", "X-Total-Count"]
allow_credentials: true
max_age: 300 # 5 minutes
>>>>>>> Stashed changes

View File

@ -0,0 +1,89 @@
# Cấu hình bảo mật cho ứng dụng
# Cấu hình CORS
cors:
# Danh sách các domain được phép truy cập (sử dụng "*" để cho phép tất cả)
allowed_origins:
- "https://example.com"
- "https://api.example.com"
# Các phương thức HTTP được phép
allowed_methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
# Các header được phép
allowed_headers:
- Origin
- Content-Type
- Content-Length
- Accept-Encoding
- X-CSRF-Token
- Authorization
- X-Requested-With
- X-Request-ID
# Các header được phép hiển thị
exposed_headers:
- Content-Length
- X-Total-Count
# Cho phép gửi credentials (cookie, authorization headers)
allow_credentials: true
# Thời gian cache preflight request (ví dụ: 5m, 1h)
max_age: 5m
# Bật chế độ debug
debug: false
# Cấu hình Rate Limiting
rate_limit:
# Số request tối đa trong khoảng thời gian
rate: 100
# Khoảng thời gian (ví dụ: 1m, 5m, 1h)
window: 1m
# Danh sách các route được bỏ qua rate limiting
excluded_routes:
- "/health"
- "/metrics"
# Cấu hình Security Headers
headers:
# Bật/tắt security headers
enabled: true
# Chính sách bảo mật nội dung (Content Security Policy)
content_security_policy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'"
# Chính sách bảo mật truy cập tài nguyên (Cross-Origin)
cross_origin_resource_policy: "same-origin"
cross_origin_opener_policy: "same-origin"
cross_origin_embedder_policy: "require-corp"
# Chính sách tham chiếu (Referrer-Policy)
referrer_policy: "no-referrer-when-downgrade"
# Chính sách sử dụng các tính năng trình duyệt (Feature-Policy)
feature_policy: "geolocation 'none'; midi 'none'; notifications 'none'; push 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; fullscreen 'none'; payment 'none'"
# Chính sách bảo vệ clickjacking (X-Frame-Options)
frame_options: "DENY"
# Chính sách bảo vệ XSS (X-XSS-Protection)
xss_protection: "1; mode=block"
# Chính sách MIME type sniffing (X-Content-Type-Options)
content_type_options: "nosniff"
# Chính sách Strict-Transport-Security (HSTS)
strict_transport_security: "max-age=31536000; includeSubDomains; preload"
# Chính sách Permissions-Policy (thay thế cho Feature-Policy)
permissions_policy: "geolocation=(), microphone=(), camera=()"

209
docs/AUTHENTICATION.md Normal file
View File

@ -0,0 +1,209 @@
# Hệ thống Xác thực và Phân quyền
## Mục lục
1. [Tổng quan](#tổng-quan)
2. [Luồng xử lý](#luồng-xử-lý)
- [Đăng nhập](#đăng-nhập)
- [Làm mới token](#làm-mới-token)
- [Đăng xuất](#đăng-xuất)
3. [Các thành phần chính](#các-thành-phần-chính)
- [Auth Middleware](#auth-middleware)
- [Token Service](#token-service)
- [Session Management](#session-management)
4. [Bảo mật](#bảo-mật)
5. [Tích hợp](#tích-hợp)
## Tổng quan
Hệ thống xác thực sử dụng JWT (JSON Web Tokens) với cơ chế refresh token để đảm bảo bảo mật. Mỗi phiên đăng nhập sẽ có:
- Access Token: Có thời hạn ngắn (15-30 phút)
- Refresh Token: Có thời hạn dài hơn (7-30 ngày)
- Session ID: Định danh duy nhất cho mỗi phiên
## Luồng xử lý
### Đăng nhập
```mermaid
sequenceDiagram
participant Client
participant AuthController
participant AuthService
participant TokenService
participant SessionStore
Client->>AuthController: POST /api/v1/auth/login
AuthController->>AuthService: Authenticate(credentials)
AuthService->>UserRepository: FindByEmail(email)
AuthService->>PasswordUtil: CompareHashAndPassword()
AuthService->>TokenService: GenerateTokens(userID, sessionID)
TokenService-->>AuthService: tokens
AuthService->>SessionStore: Create(session)
AuthService-->>AuthController: authResponse
AuthController-->>Client: {accessToken, refreshToken, user}
```
### Làm mới Token
```mermaid
sequenceDiagram
participant Client
participant AuthController
participant TokenService
participant SessionStore
Client->>AuthController: POST /api/v1/auth/refresh
AuthController->>TokenService: RefreshToken(refreshToken)
TokenService->>SessionStore: Get(sessionID)
SessionStore-->>TokenService: session
TokenService->>TokenService: ValidateRefreshToken()
TokenService->>SessionStore: UpdateLastUsed()
TokenService-->>AuthController: newTokens
AuthController-->>Client: {accessToken, refreshToken}
```
### Đăng xuất
```mermaid
sequenceDiagram
participant Client
participant AuthController
participant TokenService
participant SessionStore
Client->>AuthController: POST /api/v1/auth/logout
AuthController->>TokenService: ExtractTokenMetadata()
TokenService-->>AuthController: tokenClaims
AuthController->>SessionStore: Delete(sessionID)
AuthController-->>Client: 200 OK
```
## Các thành phần chính
### Auth Middleware
#### `Authenticate()`
- **Mục đích**: Xác thực access token trong header Authorization
- **Luồng xử lý**:
1. Lấy token từ header
2. Xác thực token
3. Kiểm tra session trong store
4. Lưu thông tin user vào context
#### `RequireRole(roles ...string)`
- **Mục đích**: Kiểm tra quyền truy cập dựa trên vai trò
- **Luồng xử lý**:
1. Lấy thông tin user từ context
2. Kiểm tra user có vai trò phù hợp không
3. Trả về lỗi nếu không có quyền
### Token Service
#### `GenerateTokens(userID, sessionID)`
- Tạo access token và refresh token
- Lưu thông tin session
- Trả về cặp token
#### `ValidateToken(token)`
- Xác thực chữ ký token
- Kiểm tra thời hạn
- Trả về claims nếu hợp lệ
### Session Management
#### `CreateSession(session)`
- Tạo session mới
- Lưu vào Redis với TTL
- Trả về session ID
#### `GetSession(sessionID)`
- Lấy thông tin session từ Redis
- Cập nhật thời gian truy cập cuối
- Trả về session nếu tồn tại
## Bảo mật
1. **Token Storage**
- Access Token: Lưu trong memory (không lưu localStorage)
- Refresh Token: HttpOnly, Secure, SameSite=Strict cookie
2. **Token Rotation**
- Mỗi lần refresh sẽ tạo cặp token mới
- Vô hiệu hóa refresh token cũ
3. **Thu hồi token**
- Đăng xuất sẽ xóa session
- Có thể thu hồi tất cả session của user
## Tích hợp
### Frontend
1. **Xử lý token**
```javascript
// Lưu token vào memory
let accessToken = null;
// Hàm gọi API với token
export const api = axios.create({
baseURL: '/api/v1',
headers: {
'Content-Type': 'application/json',
},
});
// Thêm interceptor để gắn token
api.interceptors.request.use(config => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// Xử lý lỗi 401
export function setupResponseInterceptor(logout) {
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { accessToken: newToken } = await refreshToken();
accessToken = newToken;
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
} catch (error) {
logout();
return Promise.reject(error);
}
}
return Promise.reject(error);
}
);
}
```
### Backend
1. **Cấu hình**
```yaml
auth:
access_token_expiry: 15m
refresh_token_expiry: 7d
jwt_secret: your-secret-key
refresh_secret: your-refresh-secret
```
2. **Sử dụng middleware**
```go
// Áp dụng auth middleware
router.Use(authMiddleware.Authenticate())
// Route yêu cầu đăng nhập
router.GET("/profile", userHandler.GetProfile)
// Route yêu cầu quyền admin
router.GET("/admin", authMiddleware.RequireRole("admin"), adminHandler.Dashboard)
```

View File

@ -1,116 +1,80 @@
# Đánh giá file `main.go`: Điểm bất thường và Đề xuất cải thiện
🚀 Cải Thiện Luồng Xác Thực Cho Project Starter-Kit
Một Starter-kit chất lượng cần có hệ thống xác thực được xây dựng trên các nguyên tắc bảo mật và thực hành tốt nhất. Dưới đây là những cải thiện quan trọng:
Dựa trên việc xem xét file `main.go` và các log liên quan, chúng ta đã xác định một số điểm trong thiết kế có thể dẫn đến hành vi khởi động không ổn định và log khó hiểu.
1. Bảo Mật Refresh Token (RT) Phía Client Ưu Tiên Hàng Đầu
Vấn đề cốt lõi: Lưu RT trong localStorage hoặc sessionStorage khiến chúng dễ bị tấn công XSS.
## I. Các điểm bất thường đã xác định
Giải pháp cho Starter-kit:
1. **`HTTPService.Start()` trả về ngay lập tức và che giấu lỗi khởi động Server:**
* **Mô tả:** Hàm `HTTPService.Start()` khởi chạy server HTTP (ví dụ: `s.server.Start()` của Gin) trong một goroutine riêng biệt và ngay lập tức trả về `nil` cho `lifecycleMgr`.
```go
// Trong HTTPService.Start()
go func() {
if err := s.server.Start(); err != nil {
logger.WithError(err).Error("HTTP server error")
}
}()
return nil // HTTPService.Start() trả về nil ngay
```
* **Hậu quả:**
* `lifecycleMgr` nhận được `nil` và cho rằng `HTTPService` đã khởi động thành công, ngay cả khi `s.server.Start()` trong goroutine gặp lỗi nghiêm trọng (ví dụ: cổng đã được sử dụng, lỗi cấu hình server) ngay sau đó.
* Lỗi thực sự của việc khởi động server HTTP không được truyền trở lại cho `lifecycleMgr`, khiến việc quản lý vòng đời và chẩn đoán lỗi trở nên khó khăn.
Sử dụng HttpOnly Cookies cho Refresh Token:
2. **Log "Application stopped" có thể xuất hiện không đúng thứ tự hoặc quá sớm:**
* **Mô tả:** Dòng log `logger.Info("Application stopped")` nằm ở cuối hàm `main()` và chỉ được thực thi sau khi `lifecycleMgr.Wait()` kết thúc.
* **Hậu quả:**
* Do `HTTPService.Start()` trả về ngay và goroutine của server có thể lỗi và thoát một cách âm thầm, `lifecycleMgr.Wait()` có thể kết thúc sớm hơn dự kiến.
* Điều này dẫn đến "race condition" trong logging: log "Application stopped" có thể xuất hiện trước cả các log từ bên trong `s.server.Start()` (như "Starting HTTP server"), gây nhầm lẫn khi phân tích log.
Bắt buộc: Starter-kit NÊN mặc định hoặc hướng dẫn rõ ràng việc sử dụng cookie HttpOnly để lưu RT. Điều này ngăn JavaScript phía client truy cập RT.
3. **`lifecycleMgr` không nhận biết được trạng thái lỗi thực sự của HTTP Server:**
* **Mô tả:** Vì `HTTPService.Start()` không phản ánh lỗi thực sự của việc khởi động server, `lifecycleMgr` không có thông tin chính xác về việc liệu `HTTPService` có đang hoạt động hay không.
* **Hậu quả:**
* Ứng dụng có thể "chết yểu" ở lần thử khởi động đầu tiên mà không có lỗi rõ ràng từ `lifecycleMgr`.
* Điều này có thể khiến các cơ chế bên ngoài (như Docker health checks hoặc trình giám sát tiến trình) phải khởi động lại container nhiều lần cho đến khi một lần khởi động ngẫu nhiên thành công (nếu lỗi mang tính tạm thời).
Access Token (AT) có thể được lưu trong bộ nhớ JavaScript (an toàn hơn localStorage cho AT có đời sống ngắn) hoặc sessionStorage nếu cần thiết cho SPA.
## II. Đề xuất cải thiện
Thiết lập cờ Secure và SameSite cho Cookie:
Mục tiêu chính của các đề xuất này là đảm bảo rằng trạng thái khởi động thực sự của các service (đặc biệt là HTTP server) được phản ánh chính xác cho `lifecycleMgr`, giúp việc quản lý vòng đời và gỡ lỗi hiệu quả hơn.
Secure: Đảm bảo cookie chỉ được gửi qua HTTPS.
1. **Cải thiện `HTTPService.Start()` để phản ánh trạng thái khởi động thực tế:**
SameSite=Strict (hoặc SameSite=Lax): Giúp chống lại tấn công CSRF. Starter-kit NÊN có cấu hình này.
* **Phương án 1: Biến `HTTPService.Start()` thành hàm Blocking (Ưu tiên nếu phù hợp):**
* Nếu `s.server.Start()` (ví dụ: `gin.Engine.Run()`) là một hàm blocking, hãy để `HTTPService.Start()` gọi trực tiếp nó mà không cần goroutine riêng.
* Nếu `s.server.Start()` trả về lỗi, `HTTPService.Start()` cũng phải trả về lỗi đó.
```go
// Đề xuất cho HTTPService.Start()
func (s *HTTPService) Start() error {
logger.Infof("Attempting to start %s on %s:%d...", s.Name(), s.cfg.Server.Host, s.cfg.Server.Port)
// Giả sử s.server.Start() là blocking và trả về lỗi nếu không khởi động được
if err := s.server.Start(); err != nil {
// Lỗi này sẽ được lifecycleMgr bắt và xử lý
return fmt.Errorf("failed to start %s: %w", s.Name(), err)
}
// Nếu s.server.Start() trả về mà không lỗi (thường chỉ khi shutdown),
// điều này có thể không bao giờ đạt tới nếu nó block mãi mãi.
// Hoặc nếu nó được thiết kế để trả về nil khi shutdown thành công từ bên ngoài.
return nil
}
```
* `lifecycleMgr` có thể sẽ cần tự quản lý việc chạy các hàm `Start()` blocking này trong các goroutine riêng của nó.
2. Quản Lý Refresh Token Phía Server Đảm Bảo An Toàn
Thực hành tốt đã có: Refresh Token Rotation (xoay vòng RT khi sử dụng) là rất tốt.
* **Phương án 2: Sử dụng Channel để giao tiếp trạng thái từ Goroutine:**
* Nếu bắt buộc phải giữ `s.server.Start()` trong goroutine bên trong `HTTPService.Start()`, hãy sử dụng một channel để goroutine đó báo cáo lại trạng thái (khởi động thành công hoặc lỗi) cho `HTTPService.Start()`.
* `HTTPService.Start()` sẽ đợi tín hiệu này và trả về lỗi tương ứng nếu có.
```go
// Đề xuất cho HTTPService.Start() với channel
func (s *HTTPService) Start() error {
logger.Infof("Attempting to start %s on %s:%d...", s.Name(), s.cfg.Server.Host, s.cfg.Server.Port)
errChan := make(chan error, 1) // Buffered channel
Cải thiện cho Starter-kit:
go func() {
// Gửi lỗi (hoặc nil nếu thành công) vào channel
// Cần đảm bảo s.server.Start() hoặc logic bao quanh nó có thể báo hiệu thành công sớm
// Ví dụ, nếu server có một callback "onListenSuccess" hoặc tương tự
err := s.server.Start() // Giả sử hàm này block cho đến khi có lỗi hoặc shutdown
if err != nil && err != stdHttp.ErrServerClosed { // stdHttp từ "net/http"
logger.WithError(err).Errorf("Error running %s in goroutine", s.Name())
errChan <- fmt.Errorf("%s run error: %w", s.Name(), err)
return
}
logger.Infof("%s goroutine finished cleanly.", s.Name())
errChan <- nil // Báo hiệu kết thúc không lỗi (hoặc shutdown bình thường)
}()
Vô hiệu hóa RT cũ NGAY LẬP TỨC khi xoay vòng: Đảm bảo RT đã sử dụng không còn giá trị.
// Đợi tín hiệu từ goroutine hoặc timeout
// Cần một cách để goroutine báo "đã sẵn sàng" thay vì chỉ đợi lỗi/kết thúc
// Ví dụ, server có thể gửi nil vào errChan ngay khi lắng nghe thành công.
// Đoạn code dưới đây là một ví dụ đơn giản hóa việc chờ lỗi khởi động ban đầu.
// Một giải pháp tốt hơn là server có cơ chế báo "ready".
select {
case err := <-errChan:
if err != nil {
return fmt.Errorf("failed to start %s (from channel): %w", s.Name(), err)
}
logger.Infof("%s reported successful start or clean exit via channel.", s.Name())
return nil // Thành công hoặc goroutine đã kết thúc không lỗi nghiêm trọng ban đầu
case <-time.After(10 * time.Second): // Timeout nếu không tín hiệu sớm
// Giả định rằng nếu không có lỗi ngay, server đang chạy.
// Đây là một giả định cần được kiểm chứng cẩn thận với hành vi của s.server.Start()
logger.Warnf("Timeout waiting for %s to signal start/error, assuming it's running or will manage its own lifecycle.", s.Name())
return nil
}
}
```
**Lưu ý quan trọng cho Phương án 2:** Cần có một cơ chế rõ ràng để goroutine của server báo hiệu rằng nó đã **khởi động và lắng nghe thành công** (ví dụ: gửi `nil` vào `errChan` ngay khi `ListenAnServe` bắt đầu thành công), chứ không chỉ đợi đến khi có lỗi hoặc server dừng hẳn.
Thu hồi RT khi Logout: Endpoint /api/v1/auth/logout PHẢI xóa hoặc đánh dấu RT là đã thu hồi trong cơ sở dữ liệu. Chỉ xóa ở client là không đủ.
2. **Đảm bảo `lifecycleMgr` xử lý lỗi khởi động Service một cách nghiêm ngặt:**
* Sau khi `HTTPService.Start()` (và các service khác) được sửa để trả về lỗi một cách chính xác, hãy đảm bảo rằng `lifecycleMgr.Start()` sẽ dừng toàn bộ quá trình nếu bất kỳ service nào không khởi động được.
```go
// Trong hàm main()
if err := lifecycleMgr.Start(); err != nil {
// Sử dụng Fatal để log lỗi và thoát ứng dụng ngay lập tức
logger.WithError(err).Fatalf("Critical failure: One or more services failed to start. Application shutting down.")
}
```
Điều này sẽ ngăn ứng dụng chạy trong trạng thái không ổn định và cung cấp thông tin lỗi rõ ràng ngay khi khởi động.
(Khuyến nghị cho Starter-kit nâng cao): Cân nhắc cơ chế phát hiện việc sử dụng RT đã bị đánh cắp (ví dụ: nếu một RT cũ được dùng lại sau khi đã xoay vòng, hãy thu hồi tất cả RT của user đó).
Bằng cách áp dụng những cải tiến này, bạn sẽ có một hệ thống khởi động mạnh mẽ hơn, dễ chẩn đoán lỗi hơn và các thông điệp log sẽ phản ánh chính xác hơn những gì đang thực sự xảy ra trong ứng dụng.
3. Tăng Cường Quy Trình Đăng Ký Nền Tảng Người Dùng
Cải thiện cho Starter-kit:
Chính Sách Mật Khẩu Tối Thiểu:
Yêu cầu độ dài mật khẩu tối thiểu (ví dụ: 8 hoặc 10 ký tự). Starter-kit NÊN có điều này.
(Tùy chọn): Khuyến khích hoặc yêu cầu kết hợp chữ hoa, chữ thường, số, ký tự đặc biệt.
Xác Thực Email (Khuyến Nghị Mạnh Mẽ):
Starter-kit NÊN bao gồm module hoặc hướng dẫn tích hợp quy trình gửi email xác thực để kích hoạt tài khoản. Điều này giúp đảm bảo email hợp lệ và là kênh liên lạc quan trọng.
4. Bảo Vệ Chống Tấn Công Đăng Nhập Lớp Phòng Thủ Cơ Bản
Cải thiện cho Starter-kit:
Rate Limiting cho Endpoint Đăng Nhập: Áp dụng giới hạn số lần thử đăng nhập thất bại (/api/v1/auth/login) dựa trên IP hoặc username/email.
Thông Báo Lỗi Chung Chung: Tránh các thông báo lỗi tiết lộ thông tin (ví dụ: "Username không tồn tại" hoặc "Sai mật khẩu"). Thay vào đó, sử dụng thông báo chung như "Tên đăng nhập hoặc mật khẩu không chính xác."
5. Thực Hành Tốt Nhất với JWT Cốt Lõi Của Xác Thực
Cải thiện cho Starter-kit:
Quản Lý Secret Key An Toàn:
Hướng dẫn lưu trữ JWT secret key trong biến môi trường (environment variables).
Tuyệt đối KHÔNG hardcode secret key trong mã nguồn.
Sử Dụng Thuật Toán Ký Mạnh:
Mặc định sử dụng thuật toán đối xứng mạnh như HS256.
Khuyến nghị và cung cấp tùy chọn cho thuật toán bất đối xứng như RS256 (yêu cầu quản lý cặp public/private key) cho các hệ thống phức tạp hơn.
Giữ Payload của Access Token Nhỏ Gọn:
Chỉ chứa thông tin cần thiết nhất (ví dụ: userId, roles).
Cân nhắc thêm iss (issuer) và aud (audience) để tăng cường xác minh token.
6. Xử Lý Lỗi và Ghi Log (Logging) An Toàn
Cải thiện cho Starter-kit:
Không Ghi Log Thông Tin Nhạy Cảm: Tuyệt đối KHÔNG ghi log Access Token, Refresh Token, hoặc mật khẩu dưới bất kỳ hình thức nào.
Ghi Log Sự Kiện An Ninh: Hướng dẫn hoặc cung cấp cơ chế ghi log các sự kiện quan trọng (đăng nhập thành công/thất bại, yêu cầu làm mới token, thay đổi mật khẩu) một cách an toàn, không kèm dữ liệu nhạy cảm, để phục vụ việc giám sát và điều tra.
Bằng cách tích hợp những cải tiến này, Starter-kit của bạn sẽ cung cấp một điểm khởi đầu vững chắc và an toàn hơn cho các nhà phát triển.

32
docs/session_20240524.md Normal file
View File

@ -0,0 +1,32 @@
# Tóm tắt phiên làm việc - 24/05/2025
## Các file đang mở
1. `docs/roadmap.md` - Đang xem mục tiêu phát triển
2. `configs/config.yaml` - File cấu hình ứng dụng
3. `docs/review.md` - Đang xem phần đánh giá code
## Các thay đổi chính trong phiên
### 1. Cập nhật Roadmap
- Đánh dấu hoàn thành các mục JWT Authentication
- Cập nhật chi tiết Giai đoạn 2 (Bảo mật và xác thực)
- Thêm timeline chi tiết cho từng tuần
### 2. Giải thích luồng xác thực
- Đã giải thích chi tiết về luồng JWT authentication
- Mô tả các endpoint chính và cách hoạt động
- Giải thích về bảo mật token và xử lý lỗi
### 3. Các lệnh đã sử dụng
- `/heyy` - Thảo luận về các bước tiếp theo
- `/yys` - Thử lưu trạng thái (không khả dụng)
## Công việc đang thực hiện
- Đang xem xét phần đánh giá code liên quan đến xử lý lỗi khởi động service
## Ghi chú
- Cần hoàn thiện phần Health Check API
- Cần triển khai API rate limiting và security headers
---
*Tự động tạo lúc: 2025-05-24 12:26*

6
go.mod
View File

@ -5,10 +5,12 @@ go 1.23.6
require (
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.20.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.10.0
go.uber.org/multierr v1.11.0
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.7
@ -57,7 +59,6 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
@ -67,6 +68,5 @@ require (
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

8
go.sum
View File

@ -146,8 +146,6 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@ -169,6 +167,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@ -294,7 +294,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/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-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-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@ -406,7 +406,7 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@ -6,9 +6,14 @@ import (
"strings"
"github.com/go-playground/validator/v10"
"github.com/joho/godotenv"
"github.com/spf13/viper"
)
const (
envFile = "./.env"
)
// ConfigLoader định nghĩa interface để load cấu hình
type ConfigLoader interface {
Load() (*Config, error)
@ -32,88 +37,95 @@ func NewConfigLoader() ConfigLoader {
}
}
// Load đọc cấu hình từ file và biến môi trường
func (l *ViperConfigLoader) Load() (*Config, error) {
// Khởi tạo viper
v := viper.New()
// Thiết lập tên config và loại
v.SetConfigName(l.configName)
v.SetConfigType(l.configType)
// Thêm các paths để tìm config
// Thêm các đường dẫn tìm kiếm file cấu hình
for _, path := range l.configPaths {
v.AddConfigPath(path)
}
// Tự động đọc biến môi trường
// 1. Đọc file cấu hình chính (bắt buộc)
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %w (searched in: %v)", err, l.configPaths)
}
// 2. Đọc từ file .env nếu tồn tại (tùy chọn)
if err := l.loadEnvFile(v); err != nil {
return nil, fmt.Errorf("error loading .env file: %w", err)
}
// 3. Cấu hình đọc biến môi trường (có thể ghi đè các giá trị từ file)
v.AutomaticEnv()
v.SetEnvPrefix(l.envPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
// Thiết lập giá trị mặc định
// 4. Đặt các giá trị mặc định tối thiểu
setDefaultValues(v)
// Đọc cấu hình
if err := v.ReadInConfig(); err != nil {
// Chỉ cảnh báo nếu không tìm thấy file, không gây lỗi
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("error reading config file: %v", err)
}
}
// Set default logger level
if os.Getenv("LOG_LEVEL") == "" {
if err := os.Setenv("LOG_LEVEL", "info"); err != nil {
return nil, fmt.Errorf("failed to set LOG_LEVEL: %v", err)
}
}
// Bind cấu hình vào struct
var config Config
if err := v.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("unable to decode config into struct: %v", err)
return nil, fmt.Errorf("unable to decode config into struct: %w", err)
}
// Validate cấu hình
if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("config validation error: %v", err)
return nil, fmt.Errorf("config validation error: %w", err)
}
return &config, nil
}
// setDefaultValues thiết lập các giá trị mặc định cho config
func setDefaultValues(v *viper.Viper) {
// App defaults
v.SetDefault("app.name", "GoStarter")
v.SetDefault("app.version", "0.1.0")
v.SetDefault("app.environment", "development")
v.SetDefault("app.log_level", "info")
v.SetDefault("app.timezone", "UTC")
// loadEnvFile đọc và xử lý file .env
func (l *ViperConfigLoader) loadEnvFile(v *viper.Viper) error {
if _, err := os.Stat(envFile); os.IsNotExist(err) {
return nil // Không có file .env cũng không phải lỗi
}
// Server defaults
v.SetDefault("server.host", "0.0.0.0")
v.SetDefault("server.port", 3000)
v.SetDefault("server.read_timeout", 15) // seconds
v.SetDefault("server.write_timeout", 15) // seconds
v.SetDefault("server.shutdown_timeout", 30) // seconds
v.SetDefault("server.trusted_proxies", []string{})
v.SetDefault("server.allow_origins", []string{"*"})
envMap, err := godotenv.Read(envFile)
if err != nil {
return fmt.Errorf("error parsing .env file: %w", err)
}
// Database defaults
v.SetDefault("database.driver", "postgres")
v.SetDefault("database.host", "localhost")
v.SetDefault("database.port", 5432)
v.SetDefault("database.max_open_conns", 25)
v.SetDefault("database.max_idle_conns", 5)
v.SetDefault("database.conn_max_lifetime", 300) // seconds
v.SetDefault("database.ssl_mode", "disable")
v.SetDefault("database.migration_path", "migrations")
for key, val := range envMap {
if key == "" {
continue
}
// Chuyển đổi key từ DB_PASSWORD thành database.password
if parts := strings.SplitN(key, "_", 2); len(parts) == 2 {
prefix := strings.ToLower(parts[0])
suffix := strings.ReplaceAll(parts[1], "_", ".")
v.Set(fmt.Sprintf("%s.%s", prefix, suffix), val)
}
// Lưu giá trị gốc cho tương thích ngược
v.Set(key, val)
if err := os.Setenv(key, val); err != nil {
return fmt.Errorf("failed to set environment variable %s: %w", key, err)
}
}
return nil
}
// validateConfig xác thực cấu hình
// setDefaultValues thiết lập các giá trị mặc định tối thiểu cần thiết
// Lưu ý: Hầu hết các giá trị mặc định nên được định nghĩa trong file config.yaml
func setDefaultValues(v *viper.Viper) {
// Chỉ đặt các giá trị mặc định thực sự cần thiết ở đây
// Các giá trị khác sẽ được lấy từ file config.yaml bắt buộc
v.SetDefault("app.environment", "development")
v.SetDefault("log_level", "info")
}
// validateConfig xác thực cấu hình sử dụng thẻ validate
func validateConfig(config *Config) error {
validate := validator.New()
return validate.Struct(config)
if err := validate.Struct(config); err != nil {
return fmt.Errorf("config validation failed: %w", err)
}
return nil
}

View File

@ -34,6 +34,17 @@ type DatabaseConfig struct {
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"`
AccessTokenExpire int `mapstructure:"access_token_expire" validate:"required,min=1"` // in minutes
RefreshTokenExpire int `mapstructure:"refresh_token_expire" validate:"required,min=1"` // in days
Algorithm string `mapstructure:"algorithm" validate:"required,oneof=HS256 HS384 HS512 RS256"`
Issuer string `mapstructure:"issuer" validate:"required"`
Audience []string `mapstructure:"audience" validate:"required,min=1"`
}
// Config là struct tổng thể chứa tất cả các cấu hình
type Config struct {
App AppConfig `mapstructure:"app" validate:"required"`

View File

@ -1,35 +1,43 @@
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"starter-kit/internal/helper/logger"
"time"
)
// Logger middleware for logging HTTP requests
// Logger là một middleware đơn giản để ghi log các request
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// Generate request ID
requestID := uuid.New().String()
c.Set("RequestID", requestID)
// Start timer
// Ghi thời gian bắt đầu xử lý request
start := time.Now()
// Process request
// Xử lý request
c.Next()
// Log request details
logger.WithFields(logger.Fields{
"request_id": requestID,
"method": c.Request.Method,
"path": c.Request.URL.Path,
"status": c.Writer.Status(),
"latency": time.Since(start).String(),
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
}).Info("HTTP Request")
// Tính thời gian xử lý
latency := time.Since(start)
// Lấy thông tin response
status := c.Writer.Status()
method := c.Request.Method
path := c.Request.URL.Path
// Ghi log
logMessage := "[GIN] " + time.Now().Format("2006/01/02 - 15:04:05") +
" | " + method +
" | " + path +
" | " + latency.String() +
" | " + c.ClientIP() +
" | " + c.Request.UserAgent()
if status >= 400 {
// Log lỗi
logMessage += " | " + c.Errors.ByType(gin.ErrorTypePrivate).String()
_, _ = gin.DefaultErrorWriter.Write([]byte(logMessage + "\n"))
} else {
// Log thông thường
_, _ = gin.DefaultWriter.Write([]byte(logMessage + "\n"))
}
}
}

View File

@ -0,0 +1,76 @@
package middleware
import "github.com/gin-gonic/gin"
// CORSConfig chứa cấu hình CORS
type CORSConfig struct {
AllowOrigins []string `yaml:"allow_origins"`
AllowMethods []string `yaml:"allow_methods"`
AllowHeaders []string `yaml:"allow_headers"`
}
// DefaultCORSConfig trả về cấu hình CORS mặc định
func DefaultCORSConfig() CORSConfig {
return CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
}
}
// CORS middleware xử lý CORS
func CORS(config CORSConfig) gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// RateLimiterConfig chứa cấu hình rate limiting
type RateLimiterConfig struct {
Rate int `yaml:"rate"` // Số request tối đa trong khoảng thời gian
}
// DefaultRateLimiterConfig trả về cấu hình rate limiting mặc định
func DefaultRateLimiterConfig() RateLimiterConfig {
return RateLimiterConfig{
Rate: 100, // 100 requests per minute
}
}
// NewRateLimiter tạo middleware rate limiting
func NewRateLimiter(config RateLimiterConfig) gin.HandlerFunc {
return func(c *gin.Context) {
// TODO: Implement rate limiting logic
c.Next()
}
}
// SecurityConfig chứa tất cả các cấu hình bảo mật
type SecurityConfig struct {
// CORS configuration
CORS CORSConfig `yaml:"cors"`
// Rate limiting configuration
RateLimit RateLimiterConfig `yaml:"rate_limit"`
}
// DefaultSecurityConfig trả về cấu hình bảo mật mặc định
func DefaultSecurityConfig() SecurityConfig {
return SecurityConfig{
CORS: DefaultCORSConfig(),
RateLimit: DefaultRateLimiterConfig(),
}
}
// Apply áp dụng tất cả các middleware bảo mật vào router
func (c *SecurityConfig) Apply(r *gin.Engine) {
// Áp dụng CORS middleware
r.Use(CORS(c.CORS))
// Áp dụng rate limiting
r.Use(NewRateLimiter(c.RateLimit))
}

View File

@ -0,0 +1,74 @@
package middleware_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"starter-kit/internal/transport/http/middleware"
)
func TestCORS(t *testing.T) {
// Tạo router mới
r := gin.New()
// Lấy cấu hình mặc định
config := middleware.DefaultSecurityConfig()
// Tùy chỉnh cấu hình CORS
config.CORS.AllowOrigins = []string{"https://example.com"}
// Áp dụng middleware
config.Apply(r)
// Thêm route test
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
})
// Tạo test server
ts := httptest.NewServer(r)
defer ts.Close()
// Test CORS
t.Run("Test CORS", func(t *testing.T) {
req, _ := http.NewRequest("GET", ts.URL+"/test", nil)
req.Header.Set("Origin", "https://example.com")
client := &http.Client{}
resp, err := client.Do(req)
assert.NoError(t, err)
defer func() {
err := resp.Body.Close()
assert.NoError(t, err, "Failed to close response body")
}()
assert.Equal(t, "*", resp.Header.Get("Access-Control-Allow-Origin"), "CORS header not set correctly")
})
}
func TestRateLimit(t *testing.T) {
// Test rate limiting (chỉ kiểm tra xem middleware có được áp dụng không)
config := middleware.DefaultSecurityConfig()
config.RateLimit.Rate = 10 // 10 requests per minute
r := gin.New()
config.Apply(r)
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
ts := httptest.NewServer(r)
defer ts.Close()
// Gửi một request để kiểm tra xem server có chạy không
resp, err := http.Get(ts.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
assert.NoError(t, err, "Failed to close response body")
}

View File

@ -22,8 +22,8 @@ func SetupRouter(cfg *config.Config) *gin.Engine {
// Recovery middleware
router.Use(gin.Recovery())
// CORS middleware nếu cần
// router.Use(middleware.CORS())
// CORS middleware
router.Use(middleware.CORS(middleware.DefaultCORSConfig()))
// Khởi tạo các handlers
healthHandler := handler.NewHealthHandler(cfg)

View File

@ -1,29 +1,32 @@
# App Configuration
APP_APP_NAME="ULFlow Starter Kit"
APP_APP_VERSION=0.1.0
APP_APP_ENVIRONMENT=development
APP_APP_TIMEZONE=Asia/Ho_Chi_Minh
APP_NAME="ULFlow Starter Kit"
APP_VERSION="0.1.0"
APP_ENVIRONMENT="development"
APP_TIMEZONE="Asia/Ho_Chi_Minh"
# Logger Configuration
APP_LOGGER_LEVEL=info # debug, info, warn, error
LOG_LEVEL="info" # debug, info, warn, error
# Server Configuration
APP_SERVER_HOST=0.0.0.0
APP_SERVER_PORT=3000
APP_SERVER_READ_TIMEOUT=15
APP_SERVER_WRITE_TIMEOUT=15
APP_SERVER_SHUTDOWN_TIMEOUT=30
APP_SERVER_ALLOW_ORIGINS=*
SERVER_HOST="0.0.0.0"
SERVER_PORT=3000
SERVER_READ_TIMEOUT=15
SERVER_WRITE_TIMEOUT=15
SERVER_SHUTDOWN_TIMEOUT=30
# Database Configuration
APP_DATABASE_DRIVER=postgres
APP_DATABASE_HOST=localhost
APP_DATABASE_PORT=5432
APP_DATABASE_USERNAME=postgres
APP_DATABASE_PASSWORD=postgres
APP_DATABASE_DATABASE=ulflow
APP_DATABASE_SSL_MODE=disable
APP_DATABASE_MAX_OPEN_CONNS=25
APP_DATABASE_MAX_IDLE_CONNS=5
APP_DATABASE_CONN_MAX_LIFETIME=300
APP_DATABASE_MIGRATION_PATH=migrations
DATABASE_DRIVER="postgres"
DATABASE_HOST="localhost"
DATABASE_PORT=5432
DATABASE_USERNAME="postgres"
DATABASE_PASSWORD="postgres"
DATABASE_NAME="ulflow"
DATABASE_SSLMODE="disable"
# JWT Configuration
JWT_SECRET="your-32-byte-base64-encoded-secret-key-here"
JWT_ACCESS_TOKEN_EXPIRE=15
JWT_REFRESH_TOKEN_EXPIRE=30
JWT_ALGORITHM="HS256"
JWT_ISSUER="ulflow-starter-kit"
JWT_AUDIENCE="ulflow-web"