Initial commit

This commit is contained in:
2025-11-17 15:04:27 +07:00
parent fb23922d31
commit a14562cfe0
14 changed files with 640 additions and 6673 deletions
+28 -26
View File
@@ -1,25 +1,26 @@
services:
# Main Application
app:
container_name: websocket-qris
build:
context: .
dockerfile: Dockerfile
target: prod
restart: unless-stopped
ports:
- "8070:8070"
- "8030:8030"
environment:
# Server Configuration
APP_ENV: production
PORT: 8070
PORT: 8030
GIN_MODE: release
# Default Database Configuration (PostgreSQL)
DB_CONNECTION: postgres
DB_USERNAME: stim
DB_PASSWORD: stim*RS54
DB_HOST: 10.10.123.165
DB_DATABASE: satu_db
DB_USERNAME: simtest
DB_PASSWORD: 12345
DB_HOST: 10.10.123.223
DB_DATABASE: simrsbackup
DB_PORT: 5432
DB_SSLMODE: disable
@@ -61,30 +62,30 @@ services:
# MYSQL_MEDICAL_SSLMODE: disable
# Keycloak Configuration
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: "true"
# 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: "true"
# BPJS Configuration
BPJS_BASEURL: https://apijkn.bpjs-kesehatan.go.id/vclaim-rest
BPJS_CONSID: 5257
BPJS_USERKEY: 4cf1cbef8c008440bbe9ef9ba789e482
BPJS_SECRETKEY: 1bV363512D
# BPJS_BASEURL: https://apijkn.bpjs-kesehatan.go.id/vclaim-rest
# BPJS_CONSID: 5257
# BPJS_USERKEY: 4cf1cbef8c008440bbe9ef9ba789e482
# BPJS_SECRETKEY: 1bV363512D
# SatuSehat Configuration
BRIDGING_SATUSEHAT_ORG_ID: 100026555
BRIDGING_SATUSEHAT_FASYAKES_ID: 3573011
BRIDGING_SATUSEHAT_CLIENT_ID: l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ
BRIDGING_SATUSEHAT_CLIENT_SECRET: Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy
BRIDGING_SATUSEHAT_AUTH_URL: https://api-satusehat.kemkes.go.id/oauth2/v1
BRIDGING_SATUSEHAT_BASE_URL: https://api-satusehat.kemkes.go.id/fhir-r4/v1
BRIDGING_SATUSEHAT_CONSENT_URL: https://api-satusehat.dto.kemkes.go.id/consent/v1
BRIDGING_SATUSEHAT_KFA_URL: https://api-satusehat.kemkes.go.id/kfa-v2
# BRIDGING_SATUSEHAT_ORG_ID: 100026555
# BRIDGING_SATUSEHAT_FASYAKES_ID: 3573011
# BRIDGING_SATUSEHAT_CLIENT_ID: l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ
# BRIDGING_SATUSEHAT_CLIENT_SECRET: Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy
# BRIDGING_SATUSEHAT_AUTH_URL: https://api-satusehat.kemkes.go.id/oauth2/v1
# BRIDGING_SATUSEHAT_BASE_URL: https://api-satusehat.kemkes.go.id/fhir-r4/v1
# BRIDGING_SATUSEHAT_CONSENT_URL: https://api-satusehat.dto.kemkes.go.id/consent/v1
# BRIDGING_SATUSEHAT_KFA_URL: https://api-satusehat.kemkes.go.id/kfa-v2
# Swagger Configuration
SWAGGER_TITLE: My Custom API Service
SWAGGER_DESCRIPTION: This is a custom API service for managing various resources
SWAGGER_TITLE: API Service QRIS
SWAGGER_DESCRIPTION: Documentation SWAGGER API Service QRIS
SWAGGER_VERSION: 2.0.0
SWAGGER_CONTACT_NAME: Support Team
SWAGGER_HOST: api.mycompany.com:8080
@@ -92,9 +93,10 @@ services:
SWAGGER_SCHEMES: https
# API Configuration
API_TITLE: API Service UJICOBA
API_DESCRIPTION: Dokumentation SWAGGER
API_TITLE: API Service UJICOBA QRIS
API_DESCRIPTION: Documentation SWAGGER
API_VERSION: 3.0.0
# WebSocket Configuration
WS_READ_TIMEOUT: 300s
WS_WRITE_TIMEOUT: 30s
+58 -1812
View File
File diff suppressed because it is too large Load Diff
+58 -1812
View File
File diff suppressed because it is too large Load Diff
+52 -1208
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -821,9 +821,7 @@
);
const ipBased = document.getElementById("ipBasedCheck").checked;
let url = `ws://meninjar.dev.rssa.id:8070/api/v1/ws?user_id=${encodeURIComponent(
userId
)}&room=${encodeURIComponent(room)}`;
let url = `ws://localhost:8080/api/v1/ws?user_id=QRIS&room=BANKJATIM`;
if (ipBased) {
url += "&ip_based=true";
+30 -30
View File
@@ -761,18 +761,18 @@ func (c *Config) Validate() error {
}
}
if c.Bpjs.BaseURL == "" {
log.Fatal("BPJS Base URL is required")
}
if c.Bpjs.ConsID == "" {
log.Fatal("BPJS Consumer ID is required")
}
if c.Bpjs.UserKey == "" {
log.Fatal("BPJS User Key is required")
}
if c.Bpjs.SecretKey == "" {
log.Fatal("BPJS Secret Key is required")
}
// if c.Bpjs.BaseURL == "" {
// log.Fatal("BPJS Base URL is required")
// }
// if c.Bpjs.ConsID == "" {
// log.Fatal("BPJS Consumer ID is required")
// }
// if c.Bpjs.UserKey == "" {
// log.Fatal("BPJS User Key is required")
// }
// if c.Bpjs.SecretKey == "" {
// log.Fatal("BPJS Secret Key is required")
// }
// Validate Keycloak configuration if enabled
if c.Keycloak.Enabled {
@@ -788,24 +788,24 @@ func (c *Config) Validate() error {
}
// Validate SatuSehat configuration
if c.SatuSehat.OrgID == "" {
log.Fatal("SatuSehat Organization ID is required")
}
if c.SatuSehat.FasyakesID == "" {
log.Fatal("SatuSehat Fasyankes ID is required")
}
if c.SatuSehat.ClientID == "" {
log.Fatal("SatuSehat Client ID is required")
}
if c.SatuSehat.ClientSecret == "" {
log.Fatal("SatuSehat Client Secret is required")
}
if c.SatuSehat.AuthURL == "" {
log.Fatal("SatuSehat Auth URL is required")
}
if c.SatuSehat.BaseURL == "" {
log.Fatal("SatuSehat Base URL is required")
}
// if c.SatuSehat.OrgID == "" {
// log.Fatal("SatuSehat Organization ID is required")
// }
// if c.SatuSehat.FasyakesID == "" {
// log.Fatal("SatuSehat Fasyankes ID is required")
// }
// if c.SatuSehat.ClientID == "" {
// log.Fatal("SatuSehat Client ID is required")
// }
// if c.SatuSehat.ClientSecret == "" {
// log.Fatal("SatuSehat Client Secret is required")
// }
// if c.SatuSehat.AuthURL == "" {
// log.Fatal("SatuSehat Auth URL is required")
// }
// if c.SatuSehat.BaseURL == "" {
// log.Fatal("SatuSehat Base URL is required")
// }
// Validate WebSocket configuration
if c.WebSocket.ReadTimeout <= 0 {
File diff suppressed because it is too large Load Diff
+134 -1
View File
@@ -3,6 +3,8 @@ package websocket
import (
"api-service/internal/config"
ws "api-service/internal/services/websocket"
"context"
"database/sql"
"fmt"
"net/http"
"time"
@@ -88,7 +90,7 @@ func (h *WebSocketHandler) TestWebSocketConnection(c *gin.Context) {
"user_id": "optional, for user identification",
"room": "optional, for room-based messaging",
},
"example": "ws://meninjar.dev.rssa.id:8070/api/v1/ws?client_id=test_client&room=test_room",
// "example": "ws://meninjar.dev.rssa.id:8070/api/v1/ws?client_id=test_client&room=test_room",
"timestamp": time.Now().Unix(),
})
}
@@ -134,3 +136,134 @@ func (h *WebSocketHandler) CleanupInactiveClients(c *gin.Context) {
"timestamp": time.Now().Unix(),
})
}
func (h *WebSocketHandler) BroadcastQris(c *gin.Context) {
var req struct {
Data map[string]interface{} `json:"data"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dbConn, err := h.hub.GetDatabaseConnection("simrs_backup")
if err != nil || dbConn == nil {
c.JSON(500, gin.H{"error": "Database connection failed"})
return
}
ip, _ := req.Data["ip"].(string)
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
posDevices, err := h.fetchPosDeviceByIP(ctx, dbConn, ip)
broadcaster := ws.NewBroadcaster(h.hub)
if err != nil || len(posDevices) == 0 {
broadcaster.BroadcastQris("qris_posdevice", map[string]interface{}{
"data": req.Data,
"posdevice": nil,
"message": "No posdevice found for this IP",
"timestamp": time.Now().Unix(),
})
c.JSON(404, gin.H{
"error": "No posdevice found for this IP",
"data": req.Data,
"timestamp": time.Now().Unix(),
})
return
}
broadcaster.BroadcastQris("qris_posdevice", map[string]interface{}{
"data": req.Data,
"posdevice": posDevices,
"timestamp": time.Now().Unix(),
})
c.JSON(200, gin.H{
"status": "broadcast sent",
"data": req.Data,
"posdevice": posDevices,
"timestamp": time.Now().Unix(),
})
}
func (h *WebSocketHandler) BroadcastCheck(c *gin.Context) {
var req struct {
Data map[string]interface{} `json:"data"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dbConn, err := h.hub.GetDatabaseConnection("simrs_backup")
if err != nil || dbConn == nil {
c.JSON(500, gin.H{"error": "Database connection failed"})
return
}
ip, _ := req.Data["ip"].(string)
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
posDevices, err := h.fetchPosDeviceByIP(ctx, dbConn, ip)
broadcaster := ws.NewBroadcaster(h.hub)
if err != nil || len(posDevices) == 0 {
broadcaster.BroadcastCheck("qris_check", map[string]interface{}{
"data": req.Data,
"posdevice": nil,
"message": "No posdevice found for this IP",
"timestamp": time.Now().Unix(),
})
c.JSON(404, gin.H{
"error": "No posdevice found for this IP",
"data": req.Data,
"timestamp": time.Now().Unix(),
})
return
}
broadcaster.BroadcastCheck("qris_check", map[string]interface{}{
"data": req.Data,
"posdevice": posDevices,
"timestamp": time.Now().Unix(),
})
c.JSON(200, gin.H{
"status": "broadcast sent",
"data": req.Data,
"posdevice": posDevices,
"timestamp": time.Now().Unix(),
})
}
type Qris struct {
PosDevice string `json:"posdevice"`
}
func (h *WebSocketHandler) fetchPosDeviceByIP(ctx context.Context, db *sql.DB, ip string) ([]string, error) {
query := `SELECT posdevice FROM m_deviceqris WHERE ip = $1 AND status = '1'`
rows, err := db.QueryContext(ctx, query, ip)
if err != nil {
return nil, err
}
defer rows.Close()
var posDevices []string
for rows.Next() {
var posDevice string
if err := rows.Scan(&posDevice); err != nil {
return nil, err
}
posDevices = append(posDevices, posDevice)
}
return posDevices, nil
}
-228
View File
@@ -1,228 +0,0 @@
package retribusi
import (
"api-service/internal/models"
"encoding/json"
"time"
)
// Retribusi represents the data structure for the retribusi table
// with proper null handling and optimized JSON marshaling
type Retribusi struct {
ID string `json:"id" db:"id"`
Status string `json:"status" db:"status"`
Sort models.NullableInt32 `json:"sort,omitempty" db:"sort"`
UserCreated models.NullableString `json:"user_created,omitempty" db:"user_created"`
DateCreated models.NullableTime `json:"date_created,omitempty" db:"date_created"`
UserUpdated models.NullableString `json:"user_updated,omitempty" db:"user_updated"`
DateUpdated models.NullableTime `json:"date_updated,omitempty" db:"date_updated"`
Jenis models.NullableString `json:"jenis,omitempty" db:"Jenis"`
Pelayanan models.NullableString `json:"pelayanan,omitempty" db:"Pelayanan"`
Dinas models.NullableString `json:"dinas,omitempty" db:"Dinas"`
KelompokObyek models.NullableString `json:"kelompok_obyek,omitempty" db:"Kelompok_obyek"`
KodeTarif models.NullableString `json:"kode_tarif,omitempty" db:"Kode_tarif"`
Tarif models.NullableString `json:"tarif,omitempty" db:"Tarif"`
Satuan models.NullableString `json:"satuan,omitempty" db:"Satuan"`
TarifOvertime models.NullableString `json:"tarif_overtime,omitempty" db:"Tarif_overtime"`
SatuanOvertime models.NullableString `json:"satuan_overtime,omitempty" db:"Satuan_overtime"`
RekeningPokok models.NullableString `json:"rekening_pokok,omitempty" db:"Rekening_pokok"`
RekeningDenda models.NullableString `json:"rekening_denda,omitempty" db:"Rekening_denda"`
Uraian1 models.NullableString `json:"uraian_1,omitempty" db:"Uraian_1"`
Uraian2 models.NullableString `json:"uraian_2,omitempty" db:"Uraian_2"`
Uraian3 models.NullableString `json:"uraian_3,omitempty" db:"Uraian_3"`
}
// Custom JSON marshaling untuk Retribusi agar NULL values tidak muncul di response
func (r Retribusi) MarshalJSON() ([]byte, error) {
type Alias Retribusi
aux := &struct {
Sort *int `json:"sort,omitempty"`
UserCreated *string `json:"user_created,omitempty"`
DateCreated *time.Time `json:"date_created,omitempty"`
UserUpdated *string `json:"user_updated,omitempty"`
DateUpdated *time.Time `json:"date_updated,omitempty"`
Jenis *string `json:"jenis,omitempty"`
Pelayanan *string `json:"pelayanan,omitempty"`
Dinas *string `json:"dinas,omitempty"`
KelompokObyek *string `json:"kelompok_obyek,omitempty"`
KodeTarif *string `json:"kode_tarif,omitempty"`
Tarif *string `json:"tarif,omitempty"`
Satuan *string `json:"satuan,omitempty"`
TarifOvertime *string `json:"tarif_overtime,omitempty"`
SatuanOvertime *string `json:"satuan_overtime,omitempty"`
RekeningPokok *string `json:"rekening_pokok,omitempty"`
RekeningDenda *string `json:"rekening_denda,omitempty"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
// Convert NullableInt32 to pointer
if r.Sort.Valid {
sort := int(r.Sort.Int32)
aux.Sort = &sort
}
if r.UserCreated.Valid {
aux.UserCreated = &r.UserCreated.String
}
if r.DateCreated.Valid {
aux.DateCreated = &r.DateCreated.Time
}
if r.UserUpdated.Valid {
aux.UserUpdated = &r.UserUpdated.String
}
if r.DateUpdated.Valid {
aux.DateUpdated = &r.DateUpdated.Time
}
if r.Jenis.Valid {
aux.Jenis = &r.Jenis.String
}
if r.Pelayanan.Valid {
aux.Pelayanan = &r.Pelayanan.String
}
if r.Dinas.Valid {
aux.Dinas = &r.Dinas.String
}
if r.KelompokObyek.Valid {
aux.KelompokObyek = &r.KelompokObyek.String
}
if r.KodeTarif.Valid {
aux.KodeTarif = &r.KodeTarif.String
}
if r.Tarif.Valid {
aux.Tarif = &r.Tarif.String
}
if r.Satuan.Valid {
aux.Satuan = &r.Satuan.String
}
if r.TarifOvertime.Valid {
aux.TarifOvertime = &r.TarifOvertime.String
}
if r.SatuanOvertime.Valid {
aux.SatuanOvertime = &r.SatuanOvertime.String
}
if r.RekeningPokok.Valid {
aux.RekeningPokok = &r.RekeningPokok.String
}
if r.RekeningDenda.Valid {
aux.RekeningDenda = &r.RekeningDenda.String
}
if r.Uraian1.Valid {
aux.Uraian1 = &r.Uraian1.String
}
if r.Uraian2.Valid {
aux.Uraian2 = &r.Uraian2.String
}
if r.Uraian3.Valid {
aux.Uraian3 = &r.Uraian3.String
}
return json.Marshal(aux)
}
// Helper methods untuk mendapatkan nilai yang aman
func (r *Retribusi) GetJenis() string {
if r.Jenis.Valid {
return r.Jenis.String
}
return ""
}
func (r *Retribusi) GetDinas() string {
if r.Dinas.Valid {
return r.Dinas.String
}
return ""
}
func (r *Retribusi) GetTarif() string {
if r.Tarif.Valid {
return r.Tarif.String
}
return ""
}
// Response struct untuk GET by ID - diperbaiki struktur
type RetribusiGetByIDResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Request struct untuk create - dioptimalkan dengan validasi
type RetribusiCreateRequest struct {
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct untuk create
type RetribusiCreateResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Update request - sama seperti create tapi dengan ID
type RetribusiUpdateRequest struct {
ID string `json:"-" validate:"required,uuid4"` // ID dari URL path
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct untuk update
type RetribusiUpdateResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Response struct untuk delete
type RetribusiDeleteResponse struct {
Message string `json:"message"`
ID string `json:"id"`
}
// Enhanced GET response dengan pagination dan aggregation
type RetribusiGetResponse struct {
Message string `json:"message"`
Data []Retribusi `json:"data"`
Meta models.MetaResponse `json:"meta"`
Summary *models.AggregateData `json:"summary,omitempty"`
}
// Filter struct untuk query parameters
type RetribusiFilter struct {
Status *string `json:"status,omitempty" form:"status"`
Jenis *string `json:"jenis,omitempty" form:"jenis"`
Dinas *string `json:"dinas,omitempty" form:"dinas"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" form:"kelompok_obyek"`
Search *string `json:"search,omitempty" form:"search"`
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
}
+66 -48
View File
@@ -3,9 +3,7 @@ package v1
import (
"api-service/internal/config"
"api-service/internal/database"
authHandlers "api-service/internal/handlers/auth"
healthcheckHandlers "api-service/internal/handlers/healthcheck"
retribusiHandlers "api-service/internal/handlers/retribusi"
websocketHandlers "api-service/internal/handlers/websocket"
"api-service/internal/middleware"
services "api-service/internal/services/auth"
@@ -22,7 +20,7 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
router := gin.New()
// Initialize auth middleware configuration
middleware.InitializeAuth(cfg)
middleware.AuthJWTMiddleware()
// Add global middleware
router.Use(middleware.CORSConfig())
@@ -90,17 +88,17 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
// =============================================================================
// Authentication routes
authHandler := authHandlers.NewAuthHandler(authService)
tokenHandler := authHandlers.NewTokenHandler(authService)
// 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)
// 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)
// v1.POST("/token/generate", tokenHandler.GenerateToken)
// v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect)
// =============================================================================
// WEBSOCKET ROUTES
@@ -123,52 +121,72 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
v1.GET("/ws", websocketHandler.HandleWebSocket)
v1.GET("/ws/test", websocketHandler.TestWebSocketConnection)
v1.GET("/ws/stats", websocketHandler.GetWebSocketStats)
v1.POST("/ws/broadcast/qris", websocketHandler.BroadcastQris)
v1.POST("/ws/broadcast/check", websocketHandler.BroadcastCheck)
// 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)
// 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)
// // 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 {
// Notify database change via WebSocket
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// fmt.Sprintf(`{"action": "created", "timestamp": "%s"}`, time.Now().Format(time.RFC3339)))
}
})
// // Trigger WebSocket notification after successful creation
// if c.Writer.Status() == 200 || c.Writer.Status() == 201 {
// // Notify database change via WebSocket
// // websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// // fmt.Sprintf(`{"action": "created", "timestamp": "%s"}`, time.Now().Format(time.RFC3339)))
// }
// })
retribusiGroup.PUT("/id/:id", func(c *gin.Context) {
// id := c.Param("id")
retribusiHandler.UpdateRetribusi(c)
// 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 {
// Notify database change via WebSocket
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// fmt.Sprintf(`{"action": "updated", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339)))
}
})
// // Trigger WebSocket notification after successful update
// if c.Writer.Status() == 200 {
// // Notify database change via WebSocket
// // websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// // fmt.Sprintf(`{"action": "updated", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339)))
// }
// })
retribusiGroup.DELETE("/id/:id", func(c *gin.Context) {
// id := c.Param("id")
retribusiHandler.DeleteRetribusi(c)
// 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 {
// Notify database change via WebSocket
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// fmt.Sprintf(`{"action": "deleted", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339)))
}
})
}
// // Trigger WebSocket notification after successful deletion
// if c.Writer.Status() == 200 {
// // Notify database change via WebSocket
// // websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// // fmt.Sprintf(`{"action": "deleted", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339)))
// }
// })
// }
// =============================================================================
// PROTECTED ROUTES (Authentication Required)
// =============================================================================
// Create protected group with configurable authentication
protected := v1.Group("/")
protected.Use(middleware.AuthJWTMiddleware()) // Use configurable authentication
// User profile (protected)
// protected.GET("/auth/me", authHandler.Me)
// Retribusi endpoints (CRUD operations - should be protected)
// protectedQris := protected.Group("/ws")
// {
// protectedQris.POST("/qris/broadcast", websocketHandler.BroadcastQris) // POST /api/v1/ws/
// protectedQris.POST("/check/broadcast", websocketHandler.BroadcastCheck) // POST /api/v1/ws/
// }
return router
}
@@ -1,6 +1,7 @@
package websocket
import (
"api-service/pkg/logger"
"time"
)
@@ -108,6 +109,64 @@ func (b *Broadcaster) BroadcastMessage(messageType string, data interface{}) {
}
}
// BroadcastQris godoc
// @Summary Broadcast a QRIS-related WebSocket message
// @Description Creates and broadcasts a WebSocket message with the specified type and data for QRIS operations
// @Tags WebSocket QRIS
// @Accept json
// @Produce json
// @Param messageType path string true "Type of the QRIS message to broadcast"
// @Param data body interface{} true "QRIS data payload for the message"
// @Success 200 {object} map[string]string "QRIS message successfully queued for broadcast"
// @Failure 500 {object} map[string]string "Failed to queue QRIS message (queue full)"
// @Router /api/v1/ws/broadcast/qris [post]
func (b *Broadcaster) BroadcastQris(messageType string, data interface{}) {
msg := NewWebSocketMessage(MessageType(messageType), data, "", "")
select {
case b.hub.messageQueue <- msg:
default:
// Antrian penuh, abaikan pesan
logger.Error("Message queue full, dropping message")
}
// Show posdevice if present
// if m, ok := data.(map[string]interface{}); ok {
// fmt.Println("BroadcastQris called with IP display: ", m["posdevice"])
// }
//tabel m_deviceqris
//kolom posdevice dari nama lokasi jadi ip display
//kolom ip dari ip simrs
}
// BroadcastCheck godoc
// @Summary Broadcast a WebSocket message
// @Description Creates and broadcasts a WebSocket message with the specified type and data
// @Tags WebSocket QRIS
// @Accept json
// @Produce json
// @Param messageType path string true "Type of the message to broadcast"
// @Param data body interface{} true "Data payload for the message"
// @Success 200 {object} map[string]string "Message successfully queued for broadcast"
// @Failure 500 {object} map[string]string "Failed to queue message (queue full)"
// @Router /api/v1/ws/broadcast/check [post]
func (b *Broadcaster) BroadcastCheck(messageType string, data interface{}) {
msg := NewWebSocketMessage(MessageType(messageType), data, "", "")
select {
case b.hub.messageQueue <- msg:
default:
// Antrian penuh, abaikan pesan
logger.Error("Message queue full, dropping message")
}
// Show posdevice if present
// if m, ok := data.(map[string]interface{}); ok {
// fmt.Println("BroadcastCheck called with IP display: ", m["posdevice"])
// }
}
// BroadcastToRoom mengirim pesan ke ruangan tertentu
func (b *Broadcaster) BroadcastToRoom(room string, messageType string, data interface{}) {
msg := NewWebSocketMessage(
+5 -5
View File
@@ -40,7 +40,7 @@ func (h *DatabaseHandler) handleDatabaseInsert(client *Client, message WebSocket
return nil
}
table, ok := data["table"].(string)
table, ok := data["m_deviceqris"].(string)
if !ok || table == "" {
client.sendErrorResponse("Invalid table name", "table is required")
return nil
@@ -55,7 +55,7 @@ func (h *DatabaseHandler) handleDatabaseInsert(client *Client, message WebSocket
// Perform actual database insert
if h.hub.dbService != nil {
// Get database connection
db, err := h.hub.GetDatabaseConnection("postgres_satudata")
db, err := h.hub.GetDatabaseConnection("simrs_backup")
if err != nil {
client.sendErrorResponse("Database connection error", err.Error())
return nil
@@ -110,14 +110,14 @@ func (h *DatabaseHandler) handleDatabaseQuery(client *Client, message WebSocketM
return nil
}
table, ok := data["table"].(string)
table, ok := data["m_deviceqris"].(string)
if !ok || table == "" {
client.sendErrorResponse("Invalid table name", "table is required")
return nil
}
// Execute query
results, err := h.hub.ExecuteDatabaseQuery("postgres_satudata", fmt.Sprintf("SELECT * FROM %s LIMIT 100", table))
results, err := h.hub.ExecuteDatabaseQuery("simrs_backup", fmt.Sprintf("SELECT * FROM %s LIMIT 100", table))
if err != nil {
client.sendErrorResponse("Database query error", err.Error())
return nil
@@ -142,7 +142,7 @@ func (h *DatabaseHandler) handleDatabaseCustomQuery(client *Client, message WebS
database, ok := data["database"].(string)
if !ok || database == "" {
database = "postgres_satudata"
database = "simrs_backup"
}
query, ok := data["query"].(string)
+3 -3
View File
@@ -64,7 +64,7 @@ type ActivityLog struct {
// DatabaseService mendefinisikan interface untuk layanan database
type DatabaseService interface {
Health() map[string]interface{}
ListDBs() []string
//ListDBs() []string
ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(channel, payload string)) error
NotifyChange(dbName, channel, payload string) error
GetDB(name string) (*sql.DB, error)
@@ -345,9 +345,9 @@ func (h *Hub) GetStats() map[string]interface{} {
// setupDatabaseListeners sets up database change listeners for real-time updates
func (h *Hub) setupDatabaseListeners() {
// Listen for changes on retribusi table
channels := []string{"retribusi_changes", "data_changes"}
channels := []string{"data_changes"}
err := h.dbService.ListenForChanges(h.ctx, "postgres_satudata", channels, func(channel, payload string) {
err := h.dbService.ListenForChanges(h.ctx, "simrs_backup", channels, func(channel, payload string) {
h.handleDatabaseChange(channel, payload)
})
+146 -96
View File
@@ -4,17 +4,67 @@ global:
enable_swagger: true
enable_logging: true
# services:
# retribusi:
# name: "Retribusi"
# category: "retribusi"
# package: "retribusi"
# description: "Retribusi service for tariff and billing management"
# base_url: ""
# timeout: 30
# retry_count: 3
services:
retribusi:
name: "Retribusi"
category: "retribusi"
package: "retribusi"
description: "Retribusi service for tariff and billing management"
qris:
name: "QRIS"
category: "qris"
package: "qris"
description: "QRIS service for QR code payment management"
base_url: ""
timeout: 30
retry_count: 3
endpoints:
qris:
description: "QRIS management"
handler_folder: "qris"
handler_file: "qris.go"
handler_name: "QRIS"
table_name: "m_deviceqris"
functions:
create:
methods: ["POST"]
path: "/broadcast/qris"
post_routes: "/broadcast/qris"
post_path: "/broadcast/qris"
model: "QrisCreateRequest"
response_model: "QrisCreateResponse"
request_model: "QrisCreateRequest"
description: "Send new QRIS broadcast"
summary: "Send QRIS"
tags: ["QRIS"]
require_auth: true
cache_enabled: false
enable_database: true
cache_ttl: 0
create:
methods: ["POST"]
path: "/broadcast/check"
post_routes: "/broadcast/check"
post_path: "/broadcast/check"
model: "QrisCreateRequest"
response_model: "QrisCreateResponse"
request_model: "QrisCreateRequest"
description: "Send new QRIS check broadcast"
summary: "Send QRIS check"
tags: ["QRIS"]
require_auth: true
cache_enabled: false
enable_database: true
cache_ttl: 0
# endpoints:
# retribusi:
# description: "Retribusi tariff management"
# handler_folder: "retribusi"
@@ -152,99 +202,99 @@ services:
# has_stats: true
# Example of another service
user:
name: "User"
category: "user"
package: "user"
description: "User management service"
base_url: ""
timeout: 30
retry_count: 3
# user:
# name: "User"
# category: "user"
# package: "user"
# description: "User management service"
# base_url: ""
# timeout: 30
# retry_count: 3
endpoints:
user:
description: "User management endpoints"
handler_folder: "retribusi"
handler_file: "user.go"
handler_name: "User"
table_name: "data_user"
functions:
list:
methods: ["GET"]
path: "/"
get_routes: "/"
get_path: "/"
model: "User"
response_model: "UserGetResponse"
description: "Get user list with pagination"
summary: "Get User List"
tags: ["User"]
require_auth: true
cache_enabled: true
enable_database: true
cache_ttl: 300
has_pagination: true
has_filter: true
has_search: true
# endpoints:
# user:
# description: "User management endpoints"
# handler_folder: "retribusi"
# handler_file: "user.go"
# handler_name: "User"
# table_name: "data_user"
# functions:
# list:
# methods: ["GET"]
# path: "/"
# get_routes: "/"
# get_path: "/"
# model: "User"
# response_model: "UserGetResponse"
# description: "Get user list with pagination"
# summary: "Get User List"
# tags: ["User"]
# require_auth: true
# cache_enabled: true
# enable_database: true
# cache_ttl: 300
# has_pagination: true
# has_filter: true
# has_search: true
get:
methods: ["GET"]
path: "/:id"
get_routes: "/:id"
get_path: "/:id"
model: "User"
response_model: "UserGetByIDResponse"
description: "Get user by ID"
summary: "Get User by ID"
tags: ["User"]
require_auth: true
cache_enabled: true
enable_database: true
cache_ttl: 300
# get:
# methods: ["GET"]
# path: "/:id"
# get_routes: "/:id"
# get_path: "/:id"
# model: "User"
# response_model: "UserGetByIDResponse"
# description: "Get user by ID"
# summary: "Get User by ID"
# tags: ["User"]
# require_auth: true
# cache_enabled: true
# enable_database: true
# cache_ttl: 300
create:
methods: ["POST"]
path: "/"
post_routes: "/"
post_path: "/"
model: "UserCreateRequest"
response_model: "UserCreateResponse"
request_model: "UserCreateRequest"
description: "Create new user"
summary: "Create User"
tags: ["User"]
require_auth: true
cache_enabled: false
enable_database: true
cache_ttl: 0
# create:
# methods: ["POST"]
# path: "/"
# post_routes: "/"
# post_path: "/"
# model: "UserCreateRequest"
# response_model: "UserCreateResponse"
# request_model: "UserCreateRequest"
# description: "Create new user"
# summary: "Create User"
# tags: ["User"]
# require_auth: true
# cache_enabled: false
# enable_database: true
# cache_ttl: 0
update:
methods: ["PUT"]
path: "/:id"
put_routes: "/:id"
put_path: "/:id"
model: "UserUpdateRequest"
response_model: "UserUpdateResponse"
request_model: "UserUpdateRequest"
description: "Update user"
summary: "Update User"
tags: ["User"]
require_auth: true
cache_enabled: false
enable_database: true
cache_ttl: 0
# update:
# methods: ["PUT"]
# path: "/:id"
# put_routes: "/:id"
# put_path: "/:id"
# model: "UserUpdateRequest"
# response_model: "UserUpdateResponse"
# request_model: "UserUpdateRequest"
# description: "Update user"
# summary: "Update User"
# tags: ["User"]
# require_auth: true
# cache_enabled: false
# enable_database: true
# cache_ttl: 0
delete:
methods: ["DELETE"]
path: "/:id"
delete_routes: "/:id"
delete_path: "/:id"
model: "User"
response_model: "UserDeleteResponse"
description: "Delete user"
summary: "Delete User"
tags: ["User"]
require_auth: true
cache_enabled: false
enable_database: true
cache_ttl: 0
# delete:
# methods: ["DELETE"]
# path: "/:id"
# delete_routes: "/:id"
# delete_path: "/:id"
# model: "User"
# response_model: "UserDeleteResponse"
# description: "Delete user"
# summary: "Delete User"
# tags: ["User"]
# require_auth: true
# cache_enabled: false
# enable_database: true
# cache_ttl: 0