websocket
This commit is contained in:
2251
client.html
Normal file
2251
client.html
Normal file
File diff suppressed because it is too large
Load Diff
1
go.mod
1
go.mod
@@ -21,6 +21,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mashingan/smapping v0.1.19
|
github.com/mashingan/smapping v0.1.19
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -134,6 +134,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"api-service/internal/config"
|
"api-service/internal/config"
|
||||||
|
|
||||||
_ "github.com/jackc/pgx/v5" // Import pgx driver
|
_ "github.com/jackc/pgx/v5" // Import pgx driver
|
||||||
|
"github.com/lib/pq"
|
||||||
_ "gorm.io/driver/postgres" // Import GORM PostgreSQL driver
|
_ "gorm.io/driver/postgres" // Import GORM PostgreSQL driver
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql" // MySQL driver for database/sql
|
_ "github.com/go-sql-driver/mysql" // MySQL driver for database/sql
|
||||||
@@ -44,6 +45,10 @@ type Service interface {
|
|||||||
Close() error
|
Close() error
|
||||||
ListDBs() []string
|
ListDBs() []string
|
||||||
GetDBType(name string) (DatabaseType, error)
|
GetDBType(name string) (DatabaseType, error)
|
||||||
|
// Tambahkan method untuk WebSocket notifications
|
||||||
|
ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error
|
||||||
|
NotifyChange(dbName, channel, payload string) error
|
||||||
|
GetPrimaryDB(name string) (*sql.DB, error) // Helper untuk get primary DB
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
@@ -54,6 +59,8 @@ type service struct {
|
|||||||
readConfigs map[string][]config.DatabaseConfig
|
readConfigs map[string][]config.DatabaseConfig
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
readBalancer map[string]int // Round-robin counter for read replicas
|
readBalancer map[string]int // Round-robin counter for read replicas
|
||||||
|
listeners map[string]*pq.Listener // Tambahkan untuk tracking listeners
|
||||||
|
listenersMu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -71,6 +78,7 @@ func New(cfg *config.Config) Service {
|
|||||||
configs: make(map[string]config.DatabaseConfig),
|
configs: make(map[string]config.DatabaseConfig),
|
||||||
readConfigs: make(map[string][]config.DatabaseConfig),
|
readConfigs: make(map[string][]config.DatabaseConfig),
|
||||||
readBalancer: make(map[string]int),
|
readBalancer: make(map[string]int),
|
||||||
|
listeners: make(map[string]*pq.Listener),
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Initializing database service...") // Log when the initialization starts
|
log.Println("Initializing database service...") // Log when the initialization starts
|
||||||
@@ -569,3 +577,123 @@ func (s *service) Close() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPrimaryDB returns primary database connection
|
||||||
|
func (s *service) GetPrimaryDB(name string) (*sql.DB, error) {
|
||||||
|
return s.GetDB(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenForChanges implements PostgreSQL LISTEN/NOTIFY for real-time updates
|
||||||
|
func (s *service) ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error {
|
||||||
|
s.mu.RLock()
|
||||||
|
config, exists := s.configs[dbName]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("database %s not found", dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only support PostgreSQL for LISTEN/NOTIFY
|
||||||
|
if DatabaseType(config.Type) != Postgres {
|
||||||
|
return fmt.Errorf("LISTEN/NOTIFY only supported for PostgreSQL databases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create connection string for listener
|
||||||
|
connStr := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
config.Database,
|
||||||
|
config.SSLMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create listener
|
||||||
|
listener := pq.NewListener(
|
||||||
|
connStr,
|
||||||
|
10*time.Second,
|
||||||
|
time.Minute,
|
||||||
|
func(ev pq.ListenerEventType, err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Database listener (%s) error: %v", dbName, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store listener for cleanup
|
||||||
|
s.listenersMu.Lock()
|
||||||
|
s.listeners[dbName] = listener
|
||||||
|
s.listenersMu.Unlock()
|
||||||
|
|
||||||
|
// Listen to specified channels
|
||||||
|
for _, channel := range channels {
|
||||||
|
err := listener.Listen(channel)
|
||||||
|
if err != nil {
|
||||||
|
listener.Close()
|
||||||
|
return fmt.Errorf("failed to listen to channel %s: %w", channel, err)
|
||||||
|
}
|
||||||
|
log.Printf("Listening to database channel: %s on %s", channel, dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening loop
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
listener.Close()
|
||||||
|
s.listenersMu.Lock()
|
||||||
|
delete(s.listeners, dbName)
|
||||||
|
s.listenersMu.Unlock()
|
||||||
|
log.Printf("Database listener for %s stopped", dbName)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case n := <-listener.Notify:
|
||||||
|
if n != nil {
|
||||||
|
callback(n.Channel, n.Extra)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(90 * time.Second):
|
||||||
|
// Send ping to keep connection alive
|
||||||
|
go func() {
|
||||||
|
if err := listener.Ping(); err != nil {
|
||||||
|
log.Printf("Listener ping failed for %s: %v", dbName, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyChange sends a notification to a PostgreSQL channel
|
||||||
|
func (s *service) NotifyChange(dbName, channel, payload string) error {
|
||||||
|
db, err := s.GetDB(dbName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get database %s: %w", dbName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's PostgreSQL
|
||||||
|
s.mu.RLock()
|
||||||
|
config, exists := s.configs[dbName]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("database %s configuration not found", dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if DatabaseType(config.Type) != Postgres {
|
||||||
|
return fmt.Errorf("NOTIFY only supported for PostgreSQL databases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute NOTIFY
|
||||||
|
query := "SELECT pg_notify($1, $2)"
|
||||||
|
_, err = db.Exec(query, channel, payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Sent notification to channel %s on %s: %s", channel, dbName, payload)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,14 @@ import (
|
|||||||
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
||||||
pesertaHandlers "api-service/internal/handlers/peserta"
|
pesertaHandlers "api-service/internal/handlers/peserta"
|
||||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||||
|
"api-service/internal/handlers/websocket"
|
||||||
websocketHandlers "api-service/internal/handlers/websocket"
|
websocketHandlers "api-service/internal/handlers/websocket"
|
||||||
"api-service/internal/middleware"
|
"api-service/internal/middleware"
|
||||||
services "api-service/internal/services/auth"
|
services "api-service/internal/services/auth"
|
||||||
"api-service/pkg/logger"
|
"api-service/pkg/logger"
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -36,30 +40,61 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
logger.Fatal("Failed to initialize auth service")
|
logger.Fatal("Failed to initialize auth service")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize database service for health check
|
// Initialize database service
|
||||||
dbService := database.New(cfg)
|
dbService := database.New(cfg)
|
||||||
|
|
||||||
// Health check endpoint
|
// Initialize WebSocket handler with enhanced features
|
||||||
|
websocketHandler := websocketHandlers.NewWebSocketHandler(cfg, dbService)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HEALTH CHECK & SYSTEM ROUTES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
healthCheckHandler := healthcheckHandlers.NewHealthCheckHandler(dbService)
|
healthCheckHandler := healthcheckHandlers.NewHealthCheckHandler(dbService)
|
||||||
sistem := router.Group("/api/sistem")
|
sistem := router.Group("/api/sistem")
|
||||||
|
{
|
||||||
sistem.GET("/health", healthCheckHandler.CheckHealth)
|
sistem.GET("/health", healthCheckHandler.CheckHealth)
|
||||||
|
sistem.GET("/databases", func(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"databases": dbService.ListDBs(),
|
||||||
|
"health": dbService.Health(),
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
sistem.GET("/info", func(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"service": "API Service v1.0.0",
|
||||||
|
"websocket_active": true,
|
||||||
|
"connected_clients": websocketHandler.GetConnectedClients(),
|
||||||
|
"databases": dbService.ListDBs(),
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SWAGGER DOCUMENTATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
// Swagger UI route
|
|
||||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(
|
router.GET("/swagger/*any", ginSwagger.WrapHandler(
|
||||||
swaggerFiles.Handler, // Models configuration
|
swaggerFiles.Handler,
|
||||||
ginSwagger.DefaultModelsExpandDepth(-1), // Hide models completely
|
ginSwagger.DefaultModelsExpandDepth(-1),
|
||||||
// ginSwagger.DefaultModelExpandDepth(0), // Keep individual models collapsed
|
ginSwagger.DeepLinking(true),
|
||||||
|
|
||||||
// General UI configuration
|
|
||||||
// ginSwagger.DocExpansion("none"), // Collapse all sections
|
|
||||||
ginSwagger.DeepLinking(true), // Enable deep linking
|
|
||||||
// ginSwagger.PersistAuthorization(true), // Persist auth between refreshes
|
|
||||||
|
|
||||||
// // Optional: Custom title
|
|
||||||
// ginSwagger.InstanceName("API Service v1.0.0"),
|
|
||||||
))
|
))
|
||||||
|
|
||||||
// API v1 group
|
// =============================================================================
|
||||||
|
// WEBSOCKET TEST CLIENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// router.GET("/websocket-test", func(c *gin.Context) {
|
||||||
|
// c.Header("Content-Type", "text/html")
|
||||||
|
// c.String(http.StatusOK, getWebSocketTestHTML())
|
||||||
|
// })
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// API v1 GROUP
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
v1 := router.Group("/api/v1")
|
v1 := router.Group("/api/v1")
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -75,15 +110,566 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
v1.POST("/auth/register", authHandler.Register)
|
v1.POST("/auth/register", authHandler.Register)
|
||||||
v1.POST("/auth/refresh", authHandler.RefreshToken)
|
v1.POST("/auth/refresh", authHandler.RefreshToken)
|
||||||
|
|
||||||
// Token generation routes (keep public if needed)
|
// Token generation routes
|
||||||
v1.POST("/token/generate", tokenHandler.GenerateToken)
|
v1.POST("/token/generate", tokenHandler.GenerateToken)
|
||||||
v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect)
|
v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect)
|
||||||
|
|
||||||
// WebSocket endpoint
|
// =============================================================================
|
||||||
websocketHandler := websocketHandlers.NewWebSocketHandler()
|
// WEBSOCKET ROUTES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Main WebSocket endpoint with enhanced features
|
||||||
v1.GET("/ws", websocketHandler.HandleWebSocket)
|
v1.GET("/ws", websocketHandler.HandleWebSocket)
|
||||||
|
|
||||||
// ============= PUBLISHED ROUTES ===============================================
|
// WebSocket management API
|
||||||
|
wsAPI := router.Group("/api/websocket")
|
||||||
|
{
|
||||||
|
// =============================================================================
|
||||||
|
// BASIC BROADCASTING
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
wsAPI.POST("/broadcast", func(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
Database string `json:"database,omitempty"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketHandler.BroadcastMessage(req.Type, req.Message)
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "broadcast sent",
|
||||||
|
"clients_count": websocketHandler.GetConnectedClients(),
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
wsAPI.POST("/broadcast/room/:room", func(c *gin.Context) {
|
||||||
|
room := c.Param("room")
|
||||||
|
var req struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketHandler.BroadcastToRoom(room, req.Type, req.Message)
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "room broadcast sent",
|
||||||
|
"room": room,
|
||||||
|
"clients_count": websocketHandler.GetRoomClientCount(room), // Fix: gunakan GetRoomClientCount
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ENHANCED CLIENT TARGETING
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
wsAPI.POST("/send/:clientId", func(c *gin.Context) {
|
||||||
|
clientID := c.Param("clientId")
|
||||||
|
var req struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketHandler.SendToClient(clientID, req.Type, req.Message)
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "message sent",
|
||||||
|
"client_id": clientID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send to client by static ID
|
||||||
|
wsAPI.POST("/send/static/:staticId", func(c *gin.Context) {
|
||||||
|
staticID := c.Param("staticId")
|
||||||
|
logger.Infof("Sending message to static client: %s", staticID)
|
||||||
|
var req struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
success := websocketHandler.SendToClientByStaticID(staticID, req.Type, req.Message)
|
||||||
|
if success {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "message sent to static client",
|
||||||
|
"static_id": staticID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "static client not found",
|
||||||
|
"static_id": staticID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Broadcast to all clients from specific IP
|
||||||
|
wsAPI.POST("/broadcast/ip/:ipAddress", func(c *gin.Context) {
|
||||||
|
ipAddress := c.Param("ipAddress")
|
||||||
|
var req struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count := websocketHandler.BroadcastToIP(ipAddress, req.Type, req.Message)
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "ip broadcast sent",
|
||||||
|
"ip_address": ipAddress,
|
||||||
|
"clients_count": count,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CLIENT INFORMATION & STATISTICS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
wsAPI.GET("/stats", func(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"connected_clients": websocketHandler.GetConnectedClients(),
|
||||||
|
"databases": dbService.ListDBs(),
|
||||||
|
"database_health": dbService.Health(),
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
wsAPI.GET("/stats/detailed", func(c *gin.Context) {
|
||||||
|
stats := websocketHandler.GetDetailedStats()
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"stats": stats,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
wsAPI.GET("/clients", func(c *gin.Context) {
|
||||||
|
clients := websocketHandler.GetAllClients()
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"clients": clients,
|
||||||
|
"count": len(clients),
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fix: Perbaiki GetClientsByIP untuk menggunakan ClientInfo
|
||||||
|
wsAPI.GET("/clients/by-ip/:ipAddress", func(c *gin.Context) {
|
||||||
|
ipAddress := c.Param("ipAddress")
|
||||||
|
client := websocketHandler.GetClientsByIP(ipAddress)
|
||||||
|
if client == nil {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "client not found",
|
||||||
|
"ip_address": ipAddress,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ClientInfo struct instead of direct field access
|
||||||
|
clientInfo := websocketHandler.GetAllClients()
|
||||||
|
var targetClientInfo *websocket.ClientInfo
|
||||||
|
for i := range clientInfo {
|
||||||
|
if clientInfo[i].ID == ipAddress {
|
||||||
|
targetClientInfo = &clientInfo[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetClientInfo == nil {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "ipAddress not found",
|
||||||
|
"client_id": ipAddress,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"client": map[string]interface{}{
|
||||||
|
"id": targetClientInfo.ID,
|
||||||
|
"static_id": targetClientInfo.StaticID,
|
||||||
|
"ip_address": targetClientInfo.IPAddress,
|
||||||
|
"user_id": targetClientInfo.UserID,
|
||||||
|
"room": targetClientInfo.Room,
|
||||||
|
"connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field
|
||||||
|
"last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field
|
||||||
|
},
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fix: Perbaiki GetClientByID response
|
||||||
|
wsAPI.GET("/client/:clientId", func(c *gin.Context) {
|
||||||
|
clientID := c.Param("clientId")
|
||||||
|
client := websocketHandler.GetClientByID(clientID)
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "client not found",
|
||||||
|
"client_id": clientID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ClientInfo struct instead of direct field access
|
||||||
|
clientInfo := websocketHandler.GetAllClients()
|
||||||
|
var targetClientInfo *websocket.ClientInfo
|
||||||
|
for i := range clientInfo {
|
||||||
|
if clientInfo[i].ID == clientID {
|
||||||
|
targetClientInfo = &clientInfo[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetClientInfo == nil {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "client not found",
|
||||||
|
"client_id": clientID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"client": map[string]interface{}{
|
||||||
|
"id": targetClientInfo.ID,
|
||||||
|
"static_id": targetClientInfo.StaticID,
|
||||||
|
"ip_address": targetClientInfo.IPAddress,
|
||||||
|
"user_id": targetClientInfo.UserID,
|
||||||
|
"room": targetClientInfo.Room,
|
||||||
|
"connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field
|
||||||
|
"last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field
|
||||||
|
},
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fix: Perbaiki GetClientByStaticID response
|
||||||
|
wsAPI.GET("/client/static/:staticId", func(c *gin.Context) {
|
||||||
|
staticID := c.Param("staticId")
|
||||||
|
client := websocketHandler.GetClientByStaticID(staticID)
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "static client not found",
|
||||||
|
"static_id": staticID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ClientInfo struct instead of direct field access
|
||||||
|
clientInfo := websocketHandler.GetAllClients()
|
||||||
|
var targetClientInfo *websocket.ClientInfo
|
||||||
|
for i := range clientInfo {
|
||||||
|
if clientInfo[i].StaticID == staticID {
|
||||||
|
targetClientInfo = &clientInfo[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetClientInfo == nil {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "static client not found",
|
||||||
|
"static_id": staticID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"client": map[string]interface{}{
|
||||||
|
"id": targetClientInfo.ID,
|
||||||
|
"static_id": targetClientInfo.StaticID,
|
||||||
|
"ip_address": targetClientInfo.IPAddress,
|
||||||
|
"user_id": targetClientInfo.UserID,
|
||||||
|
"room": targetClientInfo.Room,
|
||||||
|
"connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field
|
||||||
|
"last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field
|
||||||
|
},
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ACTIVE CLIENTS & CLEANUP
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Tambahkan endpoint untuk active clients
|
||||||
|
wsAPI.GET("/clients/active", func(c *gin.Context) {
|
||||||
|
// Default: clients active dalam 5 menit terakhir
|
||||||
|
minutes := c.DefaultQuery("minutes", "5")
|
||||||
|
minutesInt, err := strconv.Atoi(minutes)
|
||||||
|
if err != nil {
|
||||||
|
minutesInt = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
activeClients := websocketHandler.GetActiveClients(time.Duration(minutesInt) * time.Minute)
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"active_clients": activeClients,
|
||||||
|
"count": len(activeClients),
|
||||||
|
"threshold_minutes": minutesInt,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tambahkan endpoint untuk cleanup inactive clients
|
||||||
|
wsAPI.POST("/cleanup/inactive", func(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
InactiveMinutes int `json:"inactive_minutes"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
req.InactiveMinutes = 30 // Default 30 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.InactiveMinutes <= 0 {
|
||||||
|
req.InactiveMinutes = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanedCount := websocketHandler.CleanupInactiveClients(time.Duration(req.InactiveMinutes) * time.Minute)
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "cleanup completed",
|
||||||
|
"cleaned_clients": cleanedCount,
|
||||||
|
"inactive_minutes": req.InactiveMinutes,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DATABASE NOTIFICATIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
wsAPI.POST("/notify/:database/:channel", func(c *gin.Context) {
|
||||||
|
database := c.Param("database")
|
||||||
|
channel := c.Param("channel")
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Payload interface{} `json:"payload"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadJSON, _ := json.Marshal(req.Payload)
|
||||||
|
err := dbService.NotifyChange(database, channel, string(payloadJSON))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
"database": database,
|
||||||
|
"channel": channel,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "notification sent",
|
||||||
|
"database": database,
|
||||||
|
"channel": channel,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test database notification
|
||||||
|
wsAPI.POST("/test-notification", func(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Database string `json:"database"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values
|
||||||
|
if req.Database == "" {
|
||||||
|
req.Database = "default"
|
||||||
|
}
|
||||||
|
if req.Channel == "" {
|
||||||
|
req.Channel = "system_changes"
|
||||||
|
}
|
||||||
|
if req.Message == "" {
|
||||||
|
req.Message = "Test notification from API"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"operation": "API_TEST",
|
||||||
|
"table": "manual_test",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"message": req.Message,
|
||||||
|
"test_data": req.Data,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadJSON, _ := json.Marshal(payload)
|
||||||
|
err := dbService.NotifyChange(req.Database, req.Channel, string(payloadJSON))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "test notification sent",
|
||||||
|
"database": req.Database,
|
||||||
|
"channel": req.Channel,
|
||||||
|
"payload": payload,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ROOM MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
wsAPI.GET("/rooms", func(c *gin.Context) {
|
||||||
|
rooms := websocketHandler.GetAllRooms()
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"rooms": rooms,
|
||||||
|
"count": len(rooms),
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
wsAPI.GET("/room/:room/clients", func(c *gin.Context) {
|
||||||
|
room := c.Param("room")
|
||||||
|
clientCount := websocketHandler.GetRoomClientCount(room)
|
||||||
|
|
||||||
|
// Get detailed room info
|
||||||
|
allRooms := websocketHandler.GetAllRooms()
|
||||||
|
roomClients := allRooms[room]
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"room": room,
|
||||||
|
"client_count": clientCount,
|
||||||
|
"clients": roomClients,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MONITORING & DEBUGGING
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
wsAPI.GET("/monitor", func(c *gin.Context) {
|
||||||
|
monitor := websocketHandler.GetMonitoringData()
|
||||||
|
c.JSON(200, monitor)
|
||||||
|
})
|
||||||
|
|
||||||
|
wsAPI.POST("/ping-client/:clientId", func(c *gin.Context) {
|
||||||
|
clientID := c.Param("clientId")
|
||||||
|
websocketHandler.SendToClient(clientID, "server_ping", map[string]interface{}{
|
||||||
|
"message": "Ping from server",
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "ping sent",
|
||||||
|
"client_id": clientID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disconnect specific client
|
||||||
|
wsAPI.POST("/disconnect/:clientId", func(c *gin.Context) {
|
||||||
|
clientID := c.Param("clientId")
|
||||||
|
success := websocketHandler.DisconnectClient(clientID)
|
||||||
|
if success {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "client disconnected",
|
||||||
|
"client_id": clientID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "client not found",
|
||||||
|
"client_id": clientID,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// BULK OPERATIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Broadcast to multiple clients
|
||||||
|
wsAPI.POST("/broadcast/bulk", func(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
ClientIDs []string `json:"client_ids"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
for _, clientID := range req.ClientIDs {
|
||||||
|
websocketHandler.SendToClient(clientID, req.Type, req.Message)
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "bulk broadcast sent",
|
||||||
|
"total_clients": len(req.ClientIDs),
|
||||||
|
"success_count": successCount,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disconnect multiple clients
|
||||||
|
wsAPI.POST("/disconnect/bulk", func(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
ClientIDs []string `json:"client_ids"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
for _, clientID := range req.ClientIDs {
|
||||||
|
if websocketHandler.DisconnectClient(clientID) {
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "bulk disconnect completed",
|
||||||
|
"total_clients": len(req.ClientIDs),
|
||||||
|
"success_count": successCount,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PUBLISHED ROUTES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
// Participant eligibility information (peserta) routes
|
// Participant eligibility information (peserta) routes
|
||||||
pesertaHandler := pesertaHandlers.NewPesertaHandler(pesertaHandlers.PesertaHandlerConfig{
|
pesertaHandler := pesertaHandlers.NewPesertaHandler(pesertaHandlers.PesertaHandlerConfig{
|
||||||
@@ -95,86 +681,106 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
pesertaGroup.GET("/nokartu/:nokartu", pesertaHandler.GetBynokartu)
|
pesertaGroup.GET("/nokartu/:nokartu", pesertaHandler.GetBynokartu)
|
||||||
pesertaGroup.GET("/nik/:nik", pesertaHandler.GetBynik)
|
pesertaGroup.GET("/nik/:nik", pesertaHandler.GetBynik)
|
||||||
|
|
||||||
// // Rujukan management endpoints (rujukan) routes
|
// Retribusi endpoints with WebSocket notifications
|
||||||
// rujukanHandler := rujukanHandlers.NewRujukanHandler(rujukanHandlers.RujukanHandlerConfig{
|
|
||||||
// BpjsConfig: cfg.Bpjs,
|
|
||||||
// Logger: *logger.Default(),
|
|
||||||
// Validator: validator.New(),
|
|
||||||
// })
|
|
||||||
// rujukanGroup := v1.Group("/rujukan")
|
|
||||||
// rujukanGroup.POST("/Rujukan/:norujukan", rujukanHandler.CreateRujukan)
|
|
||||||
// rujukanGroup.PUT("/Rujukan/:norujukan", rujukanHandler.UpdateRujukan)
|
|
||||||
// rujukanGroup.DELETE("/Rujukan/:norujukan", rujukanHandler.DeleteRujukan)
|
|
||||||
// rujukanGroup.POST("/Rujukanbalik/:norujukan", rujukanHandler.CreateRujukanbalik)
|
|
||||||
// rujukanGroup.PUT("/Rujukanbalik/:norujukan", rujukanHandler.UpdateRujukanbalik)
|
|
||||||
// rujukanGroup.DELETE("/Rujukanbalik/:norujukan", rujukanHandler.DeleteRujukanbalik)
|
|
||||||
|
|
||||||
// // Search for rujukan endpoints (search) routes
|
|
||||||
// searchHandler := rujukanHandlers.NewSearchHandler(rujukanHandlers.SearchHandlerConfig{
|
|
||||||
// BpjsConfig: cfg.Bpjs,
|
|
||||||
// Logger: *logger.Default(),
|
|
||||||
// Validator: validator.New(),
|
|
||||||
// })
|
|
||||||
// searchGroup := v1.Group("/search")
|
|
||||||
// searchGroup.GET("/bynorujukan/:norujukan", searchHandler.GetBynorujukan)
|
|
||||||
// searchGroup.GET("/bynokartu/:nokartu", searchHandler.GetBynokartu)
|
|
||||||
|
|
||||||
// // Retribusi endpoints
|
|
||||||
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||||
retribusiGroup := v1.Group("/retribusi")
|
retribusiGroup := v1.Group("/retribusi")
|
||||||
{
|
{
|
||||||
retribusiGroup.GET("", retribusiHandler.GetRetribusi)
|
retribusiGroup.GET("", retribusiHandler.GetRetribusi)
|
||||||
retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) // Route baru
|
retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic)
|
||||||
retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) // Route pencarian
|
retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced)
|
||||||
retribusiGroup.GET("/id/:id", retribusiHandler.GetRetribusiByID)
|
retribusiGroup.GET("/id/:id", retribusiHandler.GetRetribusiByID)
|
||||||
retribusiGroup.POST("", retribusiHandler.CreateRetribusi)
|
|
||||||
retribusiGroup.PUT("/id/:id", retribusiHandler.UpdateRetribusi)
|
// POST/PUT/DELETE with automatic WebSocket notifications
|
||||||
retribusiGroup.DELETE("/id/:id", retribusiHandler.DeleteRetribusi)
|
retribusiGroup.POST("", func(c *gin.Context) {
|
||||||
|
retribusiHandler.CreateRetribusi(c)
|
||||||
|
|
||||||
|
// Trigger WebSocket notification after successful creation
|
||||||
|
if c.Writer.Status() == 200 || c.Writer.Status() == 201 {
|
||||||
|
websocketHandler.BroadcastMessage("retribusi_created", map[string]interface{}{
|
||||||
|
"message": "New retribusi record created",
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
retribusiGroup.PUT("/id/:id", func(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
retribusiHandler.UpdateRetribusi(c)
|
||||||
|
|
||||||
|
// Trigger WebSocket notification after successful update
|
||||||
|
if c.Writer.Status() == 200 {
|
||||||
|
websocketHandler.BroadcastMessage("retribusi_updated", map[string]interface{}{
|
||||||
|
"message": "Retribusi record updated",
|
||||||
|
"id": id,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
retribusiGroup.DELETE("/id/:id", func(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
retribusiHandler.DeleteRetribusi(c)
|
||||||
|
|
||||||
|
// Trigger WebSocket notification after successful deletion
|
||||||
|
if c.Writer.Status() == 200 {
|
||||||
|
websocketHandler.BroadcastMessage("retribusi_deleted", map[string]interface{}{
|
||||||
|
"message": "Retribusi record deleted",
|
||||||
|
"id": id,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// PROTECTED ROUTES (Authentication Required)
|
// PROTECTED ROUTES (Authentication Required)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// Create protected group with configurable authentication
|
|
||||||
protected := v1.Group("/")
|
protected := v1.Group("/")
|
||||||
protected.Use(middleware.ConfigurableAuthMiddleware(cfg)) // Use configurable authentication
|
protected.Use(middleware.ConfigurableAuthMiddleware(cfg))
|
||||||
|
|
||||||
// User profile (protected)
|
// Protected WebSocket management (optional)
|
||||||
// protected.GET("/auth/me", authHandler.Me)
|
protectedWS := protected.Group("/ws-admin")
|
||||||
|
{
|
||||||
|
protectedWS.GET("/stats", func(c *gin.Context) {
|
||||||
|
detailedStats := websocketHandler.GetDetailedStats()
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"admin_stats": detailedStats,
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// // Retribusi endpoints (CRUD operations - should be protected)
|
protectedWS.POST("/force-disconnect/:clientId", func(c *gin.Context) {
|
||||||
// retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
clientID := c.Param("clientId")
|
||||||
// protectedRetribusi := protected.Group("/retribusi")
|
success := websocketHandler.DisconnectClient(clientID)
|
||||||
// {
|
c.JSON(200, gin.H{
|
||||||
// protectedRetribusi.GET("", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi
|
"status": "force disconnect attempted",
|
||||||
// protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id
|
"client_id": clientID,
|
||||||
// protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/
|
"success": success,
|
||||||
// protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id
|
"timestamp": time.Now().Unix(),
|
||||||
// protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id
|
})
|
||||||
// }
|
})
|
||||||
|
|
||||||
// // BPJS VClaim endpoints (require authentication)
|
protectedWS.POST("/cleanup/force", func(c *gin.Context) {
|
||||||
// // Peserta routes
|
var req struct {
|
||||||
// pesertaHandler := peserta.NewVClaimHandler(peserta.VClaimHandlerConfig{
|
InactiveMinutes int `json:"inactive_minutes"`
|
||||||
// BpjsConfig: cfg.Bpjs,
|
Force bool `json:"force"`
|
||||||
// Logger: *logger.Default(),
|
}
|
||||||
// Validator: nil,
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
// })
|
req.InactiveMinutes = 10
|
||||||
// protectedPeserta := protected.Group("/peserta")
|
req.Force = false
|
||||||
// protectedPeserta.GET("/peserta/:nokartu", pesertaHandler.GetPesertaBynokartu)
|
}
|
||||||
// protectedPeserta.GET("/peserta/nik/:nik", pesertaHandler.GetPesertaBynik)
|
|
||||||
|
|
||||||
// // Sep routes
|
cleanedCount := websocketHandler.CleanupInactiveClients(time.Duration(req.InactiveMinutes) * time.Minute)
|
||||||
// sepHandler := sep.NewVClaimHandler(sep.VClaimHandlerConfig{
|
c.JSON(200, gin.H{
|
||||||
// BpjsConfig: cfg.Bpjs,
|
"status": "admin cleanup completed",
|
||||||
// Logger: *logger.Default(),
|
"cleaned_clients": cleanedCount,
|
||||||
// Validator: nil,
|
"inactive_minutes": req.InactiveMinutes,
|
||||||
// })
|
"force": req.Force,
|
||||||
// protectedSep := protected.Group("/sep")
|
"timestamp": time.Now().Unix(),
|
||||||
// protectedSep.GET("/sep/:nosep", sepHandler.GetSepSep)
|
})
|
||||||
// protectedSep.POST("/sep", sepHandler.CreateSepSep)
|
})
|
||||||
// protectedSep.PUT("/sep/:nosep", sepHandler.UpdateSepSep)
|
}
|
||||||
// protectedSep.DELETE("/sep/:nosep", sepHandler.DeleteSepSep)
|
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user