Perbaikan Middelware dan tool generete

This commit is contained in:
2025-08-21 22:44:55 +07:00
parent fb41d7720e
commit 80dafd2a69
5 changed files with 133 additions and 39 deletions

View File

@@ -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

View 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)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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