Perbaikan Middelware dan tool generete
This commit is contained in:
@@ -62,7 +62,7 @@ MYSQL_MEDICAL_SSLMODE=disable
|
||||
KEYCLOAK_ISSUER=https://auth.rssa.top/realms/sandbox
|
||||
KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran
|
||||
KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs
|
||||
KEYCLOAK_ENABLED=false
|
||||
KEYCLOAK_ENABLED=true
|
||||
|
||||
# BPJS Configuration
|
||||
BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest
|
||||
|
||||
59
internal/middleware/auth_middleware.go
Normal file
59
internal/middleware/auth_middleware.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"api-service/internal/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ConfigurableAuthMiddleware provides flexible authentication based on configuration
|
||||
func ConfigurableAuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Skip authentication for development/testing if explicitly disabled
|
||||
if !cfg.Keycloak.Enabled {
|
||||
fmt.Println("Authentication is disabled - allowing all requests")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Use Keycloak authentication when enabled
|
||||
AuthMiddleware()(c)
|
||||
}
|
||||
}
|
||||
|
||||
// StrictAuthMiddleware enforces authentication regardless of Keycloak.Enabled setting
|
||||
func StrictAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if appConfig == nil {
|
||||
fmt.Println("AuthMiddleware: Config not initialized")
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "authentication service not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
// Always enforce authentication
|
||||
AuthMiddleware()(c)
|
||||
}
|
||||
}
|
||||
|
||||
// OptionalKeycloakAuthMiddleware allows requests but adds authentication info if available
|
||||
func OptionalKeycloakAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if appConfig == nil || !appConfig.Keycloak.Enabled {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
// No token provided, but continue
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Try to validate token, but don't fail if invalid
|
||||
AuthMiddleware()(c)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/sync/singleflight"
|
||||
@@ -22,24 +24,19 @@ var (
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
)
|
||||
|
||||
// Configurable Keycloak parameters - replace with your actual values or load from config/env
|
||||
const (
|
||||
KeycloakIssuer = "https://keycloak.example.com/auth/realms/yourrealm"
|
||||
KeycloakAudience = "your-client-id"
|
||||
JwksURL = KeycloakIssuer + "/protocol/openid-connect/certs"
|
||||
)
|
||||
|
||||
// JwksCache caches JWKS keys with expiration
|
||||
type JwksCache struct {
|
||||
mu sync.RWMutex
|
||||
keys map[string]*rsa.PublicKey
|
||||
expiresAt time.Time
|
||||
sfGroup singleflight.Group
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewJwksCache() *JwksCache {
|
||||
func NewJwksCache(cfg *config.Config) *JwksCache {
|
||||
return &JwksCache{
|
||||
keys: make(map[string]*rsa.PublicKey),
|
||||
keys: make(map[string]*rsa.PublicKey),
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +71,17 @@ func (c *JwksCache) GetKey(kid string) (*rsa.PublicKey, error) {
|
||||
}
|
||||
|
||||
func (c *JwksCache) fetchKeys() (map[string]*rsa.PublicKey, error) {
|
||||
resp, err := http.Get(JwksURL)
|
||||
if !c.config.Keycloak.Enabled {
|
||||
return nil, fmt.Errorf("keycloak authentication is disabled")
|
||||
}
|
||||
|
||||
jwksURL := c.config.Keycloak.JwksURL
|
||||
if jwksURL == "" {
|
||||
// Construct JWKS URL from issuer if not explicitly provided
|
||||
jwksURL = c.config.Keycloak.Issuer + "/protocol/openid-connect/certs"
|
||||
}
|
||||
|
||||
resp, err := http.Get(jwksURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -138,11 +145,32 @@ func base64UrlDecode(s string) ([]byte, error) {
|
||||
return base64.URLEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
var jwksCache = NewJwksCache()
|
||||
// Global config instance
|
||||
var appConfig *config.Config
|
||||
var jwksCacheInstance *JwksCache
|
||||
|
||||
// InitializeAuth initializes the auth middleware with config
|
||||
func InitializeAuth(cfg *config.Config) {
|
||||
appConfig = cfg
|
||||
jwksCacheInstance = NewJwksCache(cfg)
|
||||
}
|
||||
|
||||
// AuthMiddleware validates Bearer token as Keycloak JWT token
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if appConfig == nil {
|
||||
fmt.Println("AuthMiddleware: Config not initialized")
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "authentication service not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
if !appConfig.Keycloak.Enabled {
|
||||
// Skip authentication if Keycloak is disabled but log for debugging
|
||||
fmt.Println("AuthMiddleware: Keycloak authentication is disabled - allowing all requests")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("AuthMiddleware: Checking Authorization header") // Debug log
|
||||
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
@@ -174,8 +202,8 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
return nil, errors.New("kid header not found")
|
||||
}
|
||||
|
||||
return jwksCache.GetKey(kid)
|
||||
}, jwt.WithIssuer(KeycloakIssuer), jwt.WithAudience(KeycloakAudience))
|
||||
return jwksCacheInstance.GetKey(kid)
|
||||
}, jwt.WithIssuer(appConfig.Keycloak.Issuer), jwt.WithAudience(appConfig.Keycloak.Audience))
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
fmt.Printf("AuthMiddleware: Invalid or expired token: %v\n", err) // Debug log
|
||||
@@ -17,6 +17,9 @@ import (
|
||||
func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
router := gin.New()
|
||||
|
||||
// Initialize auth middleware configuration
|
||||
middleware.InitializeAuth(cfg)
|
||||
|
||||
// Add global middleware
|
||||
router.Use(middleware.CORSConfig())
|
||||
router.Use(middleware.ErrorHandler())
|
||||
@@ -55,39 +58,42 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
// BPJS endpoints
|
||||
bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
|
||||
v1.GET("/bpjs/peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK)
|
||||
// Retribusi endpoints
|
||||
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||
retribusiGroup := v1.Group("/retribusi")
|
||||
{
|
||||
retribusiGroup.GET("", retribusiHandler.GetRetribusi)
|
||||
retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) // Route baru
|
||||
retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) // Route pencarian
|
||||
retribusiGroup.GET("/:id", retribusiHandler.GetRetribusiByID)
|
||||
retribusiGroup.POST("", retribusiHandler.CreateRetribusi)
|
||||
retribusiGroup.PUT("/:id", retribusiHandler.UpdateRetribusi)
|
||||
retribusiGroup.DELETE("/:id", retribusiHandler.DeleteRetribusi)
|
||||
}
|
||||
|
||||
// ============= PUBLISHED ROUTES ===============================================
|
||||
|
||||
// // Retribusi endpoints
|
||||
// retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||
// retribusiGroup := v1.Group("/retribusi")
|
||||
// {
|
||||
// retribusiGroup.GET("", retribusiHandler.GetRetribusi)
|
||||
// retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) // Route baru
|
||||
// retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) // Route pencarian
|
||||
// retribusiGroup.GET("/:id", retribusiHandler.GetRetribusiByID)
|
||||
// retribusiGroup.POST("", retribusiHandler.CreateRetribusi)
|
||||
// retribusiGroup.PUT("/:id", retribusiHandler.UpdateRetribusi)
|
||||
// retribusiGroup.DELETE("/:id", retribusiHandler.DeleteRetribusi)
|
||||
// }
|
||||
// =============================================================================
|
||||
// PROTECTED ROUTES (Authentication Required)
|
||||
// =============================================================================
|
||||
|
||||
// Create protected group with AuthMiddleware
|
||||
// Create protected group with configurable authentication
|
||||
protected := v1.Group("/")
|
||||
protected.Use(middleware.AuthMiddleware()) // Use Keycloak AuthMiddleware
|
||||
protected.Use(middleware.ConfigurableAuthMiddleware(cfg)) // Use configurable authentication
|
||||
|
||||
// User profile (protected)
|
||||
protected.GET("/auth/me", authHandler.Me)
|
||||
|
||||
// Retribusi endpoints (CRUD operations - should be protected)
|
||||
// retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||
// protectedRetribusi := protected.Group("/retribusi")
|
||||
// {
|
||||
// protectedRetribusi.GET("", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi
|
||||
// protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id
|
||||
// protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/
|
||||
// protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id
|
||||
// protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id
|
||||
// }
|
||||
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||
protectedRetribusi := protected.Group("/retribusi")
|
||||
{
|
||||
protectedRetribusi.GET("", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi
|
||||
protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id
|
||||
protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/
|
||||
protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id
|
||||
protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id
|
||||
}
|
||||
|
||||
// BPJS endpoints (sensitive data - should be protected)
|
||||
// bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
|
||||
|
||||
@@ -117,7 +117,7 @@ func main() {
|
||||
// Create directories with improved logic
|
||||
var handlerDir, modelDir string
|
||||
if category != "" {
|
||||
handlerDir = filepath.Join("internal", "handlers")
|
||||
handlerDir = filepath.Join("internal", "handlers", category)
|
||||
modelDir = filepath.Join("internal", "models", category)
|
||||
} else {
|
||||
handlerDir = filepath.Join("internal", "handlers")
|
||||
@@ -248,6 +248,7 @@ func New` + data.Name + `Handler() *` + data.Name + `Handler {
|
||||
handlerContent += generateHelperMethods(data)
|
||||
|
||||
writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent)
|
||||
|
||||
}
|
||||
|
||||
func generateGetMethods(data HandlerData) string {
|
||||
@@ -1490,7 +1491,7 @@ func updateRoutesFile(data HandlerData) {
|
||||
newRoutes := generateProtectedRouteBlock(data)
|
||||
|
||||
// Insert above protected routes marker
|
||||
insertMarker := "// ============================================================================="
|
||||
insertMarker := "// ============= PUBLISHED ROUTES ==============================================="
|
||||
if strings.Contains(routesContent, insertMarker) {
|
||||
if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) {
|
||||
// Insert before the marker
|
||||
|
||||
Reference in New Issue
Block a user