package v1 import ( "api-service/internal/config" "api-service/internal/database" authHandlers "api-service/internal/handlers/auth" healthcheckHandlers "api-service/internal/handlers/healthcheck" kioskListkioskHandlers "api-service/internal/handlers/kiosk" patientPatientHandlers "api-service/internal/handlers/patient" pesertaHandlers "api-service/internal/handlers/peserta" retribusiHandlers "api-service/internal/handlers/retribusi" "api-service/internal/handlers/websocket" websocketHandlers "api-service/internal/handlers/websocket" "api-service/internal/middleware" services "api-service/internal/services/auth" "api-service/pkg/logger" "encoding/json" "strconv" "time" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) 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()) router.Use(logger.RequestLoggerMiddleware(logger.Default())) router.Use(gin.Recovery()) // Initialize services with error handling authService := services.NewAuthService(cfg) if authService == nil { logger.Fatal("Failed to initialize auth service") } // Initialize database service dbService := database.New(cfg) // Initialize WebSocket handler with enhanced features websocketHandler := websocketHandlers.NewWebSocketHandler(cfg, dbService) // ============================================================================= // HEALTH CHECK & SYSTEM ROUTES // ============================================================================= healthCheckHandler := healthcheckHandlers.NewHealthCheckHandler(dbService) sistem := router.Group("/api/sistem") { 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 // ============================================================================= router.GET("/swagger/*any", ginSwagger.WrapHandler( swaggerFiles.Handler, ginSwagger.DefaultModelsExpandDepth(-1), ginSwagger.DeepLinking(true), )) // ============================================================================= // 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") // ============================================================================= // PUBLIC ROUTES (No Authentication Required) // ============================================================================= // Authentication routes authHandler := authHandlers.NewAuthHandler(authService) tokenHandler := authHandlers.NewTokenHandler(authService) // Basic auth routes v1.POST("/auth/login", authHandler.Login) v1.POST("/auth/register", authHandler.Register) v1.POST("/auth/refresh", authHandler.RefreshToken) // Token generation routes v1.POST("/token/generate", tokenHandler.GenerateToken) v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect) // ============================================================================= // WEBSOCKET ROUTES // ============================================================================= // Main WebSocket endpoint with enhanced features v1.GET("/ws", websocketHandler.HandleWebSocket) // 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 pesertaHandler := pesertaHandlers.NewPesertaHandler(pesertaHandlers.PesertaHandlerConfig{ Config: cfg, Logger: *logger.Default(), Validator: validator.New(), }) pesertaGroup := v1.Group("/Peserta") pesertaGroup.GET("/nokartu/:nokartu", pesertaHandler.GetBynokartu) pesertaGroup.GET("/nik/:nik", pesertaHandler.GetBynik) // Retribusi endpoints with WebSocket notifications retribusiHandler := retribusiHandlers.NewRetribusiHandler() retribusiGroup := v1.Group("/retribusi") { retribusiGroup.GET("", retribusiHandler.GetRetribusi) retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) retribusiGroup.GET("/id/:id", retribusiHandler.GetRetribusiByID) // POST/PUT/DELETE with automatic WebSocket notifications 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 := v1.Group("/") protected.Use(middleware.ConfigurableAuthMiddleware(cfg)) // Protected WebSocket management (optional) 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(), }) }) protectedWS.POST("/force-disconnect/:clientId", func(c *gin.Context) { clientID := c.Param("clientId") success := websocketHandler.DisconnectClient(clientID) c.JSON(200, gin.H{ "status": "force disconnect attempted", "client_id": clientID, "success": success, "timestamp": time.Now().Unix(), }) }) protectedWS.POST("/cleanup/force", func(c *gin.Context) { var req struct { InactiveMinutes int `json:"inactive_minutes"` Force bool `json:"force"` } if err := c.ShouldBindJSON(&req); err != nil { req.InactiveMinutes = 10 req.Force = false } cleanedCount := websocketHandler.CleanupInactiveClients(time.Duration(req.InactiveMinutes) * time.Minute) c.JSON(200, gin.H{ "status": "admin cleanup completed", "cleaned_clients": cleanedCount, "inactive_minutes": req.InactiveMinutes, "force": req.Force, "timestamp": time.Now().Unix(), }) }) } // Listkiosk endpoints kioskListkioskHandler := kioskListkioskHandlers.NewListkioskHandler() kioskListkioskGroup := v1.Group("/kiosk") { kioskListkioskGroup.GET("/listkiosk", kioskListkioskHandler.GetListkiosk) //kioskListkioskGroup.GET("/:id", kioskListkioskHandler.GetListkioskByID) kioskListkioskGroup.POST("", kioskListkioskHandler.CreateKiosk) kioskListkioskGroup.PUT("/:id", kioskListkioskHandler.UpdateKiosk) kioskListkioskGroup.DELETE("/:id", kioskListkioskHandler.DeleteKiosk) kioskListkioskGroup.GET("/stats", kioskListkioskHandler.GetKioskStats) } // Patient endpoints patientPatientHandler := patientPatientHandlers.NewPatientHandler() patientPatientGroup := v1.Group("/patient") { patientPatientGroup.GET("/listpatient", patientPatientHandler.GetPatient) patientPatientGroup.POST("/:id", patientPatientHandler.GetPatientByIDPost) patientPatientGroup.POST("", patientPatientHandler.CreatePatient) patientPatientGroup.PUT("/:id", patientPatientHandler.UpdatePatient) patientPatientGroup.DELETE("/:id", patientPatientHandler.DeletePatient) patientPatientGroup.GET("/stats", patientPatientHandler.GetPatientStats) } return router }