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: services:
# Main Application # Main Application
app: app:
container_name: websocket-qris
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: prod target: prod
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8070:8070" - "8030:8030"
environment: environment:
# Server Configuration # Server Configuration
APP_ENV: production APP_ENV: production
PORT: 8070 PORT: 8030
GIN_MODE: release GIN_MODE: release
# Default Database Configuration (PostgreSQL) # Default Database Configuration (PostgreSQL)
DB_CONNECTION: postgres DB_CONNECTION: postgres
DB_USERNAME: stim DB_USERNAME: simtest
DB_PASSWORD: stim*RS54 DB_PASSWORD: 12345
DB_HOST: 10.10.123.165 DB_HOST: 10.10.123.223
DB_DATABASE: satu_db DB_DATABASE: simrsbackup
DB_PORT: 5432 DB_PORT: 5432
DB_SSLMODE: disable DB_SSLMODE: disable
@@ -61,30 +62,30 @@ services:
# MYSQL_MEDICAL_SSLMODE: disable # MYSQL_MEDICAL_SSLMODE: disable
# Keycloak Configuration # Keycloak Configuration
KEYCLOAK_ISSUER: https://auth.rssa.top/realms/sandbox # KEYCLOAK_ISSUER: https://auth.rssa.top/realms/sandbox
KEYCLOAK_AUDIENCE: nuxtsim-pendaftaran # KEYCLOAK_AUDIENCE: nuxtsim-pendaftaran
KEYCLOAK_JWKS_URL: https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs # KEYCLOAK_JWKS_URL: https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs
KEYCLOAK_ENABLED: "true" # KEYCLOAK_ENABLED: "true"
# BPJS Configuration # BPJS Configuration
BPJS_BASEURL: https://apijkn.bpjs-kesehatan.go.id/vclaim-rest # BPJS_BASEURL: https://apijkn.bpjs-kesehatan.go.id/vclaim-rest
BPJS_CONSID: 5257 # BPJS_CONSID: 5257
BPJS_USERKEY: 4cf1cbef8c008440bbe9ef9ba789e482 # BPJS_USERKEY: 4cf1cbef8c008440bbe9ef9ba789e482
BPJS_SECRETKEY: 1bV363512D # BPJS_SECRETKEY: 1bV363512D
# SatuSehat Configuration # SatuSehat Configuration
BRIDGING_SATUSEHAT_ORG_ID: 100026555 # BRIDGING_SATUSEHAT_ORG_ID: 100026555
BRIDGING_SATUSEHAT_FASYAKES_ID: 3573011 # BRIDGING_SATUSEHAT_FASYAKES_ID: 3573011
BRIDGING_SATUSEHAT_CLIENT_ID: l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ # BRIDGING_SATUSEHAT_CLIENT_ID: l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ
BRIDGING_SATUSEHAT_CLIENT_SECRET: Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy # BRIDGING_SATUSEHAT_CLIENT_SECRET: Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy
BRIDGING_SATUSEHAT_AUTH_URL: https://api-satusehat.kemkes.go.id/oauth2/v1 # 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_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_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_KFA_URL: https://api-satusehat.kemkes.go.id/kfa-v2
# Swagger Configuration # Swagger Configuration
SWAGGER_TITLE: My Custom API Service SWAGGER_TITLE: API Service QRIS
SWAGGER_DESCRIPTION: This is a custom API service for managing various resources SWAGGER_DESCRIPTION: Documentation SWAGGER API Service QRIS
SWAGGER_VERSION: 2.0.0 SWAGGER_VERSION: 2.0.0
SWAGGER_CONTACT_NAME: Support Team SWAGGER_CONTACT_NAME: Support Team
SWAGGER_HOST: api.mycompany.com:8080 SWAGGER_HOST: api.mycompany.com:8080
@@ -92,9 +93,10 @@ services:
SWAGGER_SCHEMES: https SWAGGER_SCHEMES: https
# API Configuration # API Configuration
API_TITLE: API Service UJICOBA API_TITLE: API Service UJICOBA QRIS
API_DESCRIPTION: Dokumentation SWAGGER API_DESCRIPTION: Documentation SWAGGER
API_VERSION: 3.0.0 API_VERSION: 3.0.0
# WebSocket Configuration # WebSocket Configuration
WS_READ_TIMEOUT: 300s WS_READ_TIMEOUT: 300s
WS_WRITE_TIMEOUT: 30s 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; const ipBased = document.getElementById("ipBasedCheck").checked;
let url = `ws://meninjar.dev.rssa.id:8070/api/v1/ws?user_id=${encodeURIComponent( let url = `ws://localhost:8080/api/v1/ws?user_id=QRIS&room=BANKJATIM`;
userId
)}&room=${encodeURIComponent(room)}`;
if (ipBased) { if (ipBased) {
url += "&ip_based=true"; url += "&ip_based=true";
+30 -30
View File
@@ -761,18 +761,18 @@ func (c *Config) Validate() error {
} }
} }
if c.Bpjs.BaseURL == "" { // if c.Bpjs.BaseURL == "" {
log.Fatal("BPJS Base URL is required") // log.Fatal("BPJS Base URL is required")
} // }
if c.Bpjs.ConsID == "" { // if c.Bpjs.ConsID == "" {
log.Fatal("BPJS Consumer ID is required") // log.Fatal("BPJS Consumer ID is required")
} // }
if c.Bpjs.UserKey == "" { // if c.Bpjs.UserKey == "" {
log.Fatal("BPJS User Key is required") // log.Fatal("BPJS User Key is required")
} // }
if c.Bpjs.SecretKey == "" { // if c.Bpjs.SecretKey == "" {
log.Fatal("BPJS Secret Key is required") // log.Fatal("BPJS Secret Key is required")
} // }
// Validate Keycloak configuration if enabled // Validate Keycloak configuration if enabled
if c.Keycloak.Enabled { if c.Keycloak.Enabled {
@@ -788,24 +788,24 @@ func (c *Config) Validate() error {
} }
// Validate SatuSehat configuration // Validate SatuSehat configuration
if c.SatuSehat.OrgID == "" { // if c.SatuSehat.OrgID == "" {
log.Fatal("SatuSehat Organization ID is required") // log.Fatal("SatuSehat Organization ID is required")
} // }
if c.SatuSehat.FasyakesID == "" { // if c.SatuSehat.FasyakesID == "" {
log.Fatal("SatuSehat Fasyankes ID is required") // log.Fatal("SatuSehat Fasyankes ID is required")
} // }
if c.SatuSehat.ClientID == "" { // if c.SatuSehat.ClientID == "" {
log.Fatal("SatuSehat Client ID is required") // log.Fatal("SatuSehat Client ID is required")
} // }
if c.SatuSehat.ClientSecret == "" { // if c.SatuSehat.ClientSecret == "" {
log.Fatal("SatuSehat Client Secret is required") // log.Fatal("SatuSehat Client Secret is required")
} // }
if c.SatuSehat.AuthURL == "" { // if c.SatuSehat.AuthURL == "" {
log.Fatal("SatuSehat Auth URL is required") // log.Fatal("SatuSehat Auth URL is required")
} // }
if c.SatuSehat.BaseURL == "" { // if c.SatuSehat.BaseURL == "" {
log.Fatal("SatuSehat Base URL is required") // log.Fatal("SatuSehat Base URL is required")
} // }
// Validate WebSocket configuration // Validate WebSocket configuration
if c.WebSocket.ReadTimeout <= 0 { 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 ( import (
"api-service/internal/config" "api-service/internal/config"
ws "api-service/internal/services/websocket" ws "api-service/internal/services/websocket"
"context"
"database/sql"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
@@ -88,7 +90,7 @@ func (h *WebSocketHandler) TestWebSocketConnection(c *gin.Context) {
"user_id": "optional, for user identification", "user_id": "optional, for user identification",
"room": "optional, for room-based messaging", "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(), "timestamp": time.Now().Unix(),
}) })
} }
@@ -134,3 +136,134 @@ func (h *WebSocketHandler) CleanupInactiveClients(c *gin.Context) {
"timestamp": time.Now().Unix(), "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 ( import (
"api-service/internal/config" "api-service/internal/config"
"api-service/internal/database" "api-service/internal/database"
authHandlers "api-service/internal/handlers/auth"
healthcheckHandlers "api-service/internal/handlers/healthcheck" healthcheckHandlers "api-service/internal/handlers/healthcheck"
retribusiHandlers "api-service/internal/handlers/retribusi"
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"
@@ -22,7 +20,7 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
router := gin.New() router := gin.New()
// Initialize auth middleware configuration // Initialize auth middleware configuration
middleware.InitializeAuth(cfg) middleware.AuthJWTMiddleware()
// Add global middleware // Add global middleware
router.Use(middleware.CORSConfig()) router.Use(middleware.CORSConfig())
@@ -90,17 +88,17 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
// ============================================================================= // =============================================================================
// Authentication routes // Authentication routes
authHandler := authHandlers.NewAuthHandler(authService) // authHandler := authHandlers.NewAuthHandler(authService)
tokenHandler := authHandlers.NewTokenHandler(authService) // tokenHandler := authHandlers.NewTokenHandler(authService)
// Basic auth routes // Basic auth routes
v1.POST("/auth/login", authHandler.Login) // v1.POST("/auth/login", authHandler.Login)
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 // 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 ROUTES // WEBSOCKET ROUTES
@@ -123,52 +121,72 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
v1.GET("/ws", websocketHandler.HandleWebSocket) v1.GET("/ws", websocketHandler.HandleWebSocket)
v1.GET("/ws/test", websocketHandler.TestWebSocketConnection) v1.GET("/ws/test", websocketHandler.TestWebSocketConnection)
v1.GET("/ws/stats", websocketHandler.GetWebSocketStats) 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 // Retribusi endpoints with WebSocket notifications
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) // retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic)
retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) // retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced)
retribusiGroup.GET("/id/:id", retribusiHandler.GetRetribusiByID) // retribusiGroup.GET("/id/:id", retribusiHandler.GetRetribusiByID)
// POST/PUT/DELETE with automatic WebSocket notifications // // POST/PUT/DELETE with automatic WebSocket notifications
retribusiGroup.POST("", func(c *gin.Context) { // retribusiGroup.POST("", func(c *gin.Context) {
retribusiHandler.CreateRetribusi(c) // retribusiHandler.CreateRetribusi(c)
// Trigger WebSocket notification after successful creation // // Trigger WebSocket notification after successful creation
if c.Writer.Status() == 200 || c.Writer.Status() == 201 { // if c.Writer.Status() == 200 || c.Writer.Status() == 201 {
// Notify database change via WebSocket // // Notify database change via WebSocket
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes", // // websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// fmt.Sprintf(`{"action": "created", "timestamp": "%s"}`, time.Now().Format(time.RFC3339))) // // fmt.Sprintf(`{"action": "created", "timestamp": "%s"}`, time.Now().Format(time.RFC3339)))
} // }
}) // })
retribusiGroup.PUT("/id/:id", func(c *gin.Context) { // retribusiGroup.PUT("/id/:id", func(c *gin.Context) {
// id := c.Param("id") // // id := c.Param("id")
retribusiHandler.UpdateRetribusi(c) // retribusiHandler.UpdateRetribusi(c)
// Trigger WebSocket notification after successful update // // Trigger WebSocket notification after successful update
if c.Writer.Status() == 200 { // if c.Writer.Status() == 200 {
// Notify database change via WebSocket // // Notify database change via WebSocket
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes", // // websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// fmt.Sprintf(`{"action": "updated", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339))) // // fmt.Sprintf(`{"action": "updated", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339)))
} // }
}) // })
retribusiGroup.DELETE("/id/:id", func(c *gin.Context) { // retribusiGroup.DELETE("/id/:id", func(c *gin.Context) {
// id := c.Param("id") // // id := c.Param("id")
retribusiHandler.DeleteRetribusi(c) // retribusiHandler.DeleteRetribusi(c)
// Trigger WebSocket notification after successful deletion // // Trigger WebSocket notification after successful deletion
if c.Writer.Status() == 200 { // if c.Writer.Status() == 200 {
// Notify database change via WebSocket // // Notify database change via WebSocket
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes", // // websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
// fmt.Sprintf(`{"action": "deleted", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339))) // // 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 return router
} }
@@ -1,6 +1,7 @@
package websocket package websocket
import ( import (
"api-service/pkg/logger"
"time" "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 // BroadcastToRoom mengirim pesan ke ruangan tertentu
func (b *Broadcaster) BroadcastToRoom(room string, messageType string, data interface{}) { func (b *Broadcaster) BroadcastToRoom(room string, messageType string, data interface{}) {
msg := NewWebSocketMessage( msg := NewWebSocketMessage(
+5 -5
View File
@@ -40,7 +40,7 @@ func (h *DatabaseHandler) handleDatabaseInsert(client *Client, message WebSocket
return nil return nil
} }
table, ok := data["table"].(string) table, ok := data["m_deviceqris"].(string)
if !ok || table == "" { if !ok || table == "" {
client.sendErrorResponse("Invalid table name", "table is required") client.sendErrorResponse("Invalid table name", "table is required")
return nil return nil
@@ -55,7 +55,7 @@ func (h *DatabaseHandler) handleDatabaseInsert(client *Client, message WebSocket
// Perform actual database insert // Perform actual database insert
if h.hub.dbService != nil { if h.hub.dbService != nil {
// Get database connection // Get database connection
db, err := h.hub.GetDatabaseConnection("postgres_satudata") db, err := h.hub.GetDatabaseConnection("simrs_backup")
if err != nil { if err != nil {
client.sendErrorResponse("Database connection error", err.Error()) client.sendErrorResponse("Database connection error", err.Error())
return nil return nil
@@ -110,14 +110,14 @@ func (h *DatabaseHandler) handleDatabaseQuery(client *Client, message WebSocketM
return nil return nil
} }
table, ok := data["table"].(string) table, ok := data["m_deviceqris"].(string)
if !ok || table == "" { if !ok || table == "" {
client.sendErrorResponse("Invalid table name", "table is required") client.sendErrorResponse("Invalid table name", "table is required")
return nil return nil
} }
// Execute query // 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 { if err != nil {
client.sendErrorResponse("Database query error", err.Error()) client.sendErrorResponse("Database query error", err.Error())
return nil return nil
@@ -142,7 +142,7 @@ func (h *DatabaseHandler) handleDatabaseCustomQuery(client *Client, message WebS
database, ok := data["database"].(string) database, ok := data["database"].(string)
if !ok || database == "" { if !ok || database == "" {
database = "postgres_satudata" database = "simrs_backup"
} }
query, ok := data["query"].(string) query, ok := data["query"].(string)
+3 -3
View File
@@ -64,7 +64,7 @@ type ActivityLog struct {
// DatabaseService mendefinisikan interface untuk layanan database // DatabaseService mendefinisikan interface untuk layanan database
type DatabaseService interface { type DatabaseService interface {
Health() map[string]interface{} Health() map[string]interface{}
ListDBs() []string //ListDBs() []string
ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(channel, payload string)) error ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(channel, payload string)) error
NotifyChange(dbName, channel, payload string) error NotifyChange(dbName, channel, payload string) error
GetDB(name string) (*sql.DB, 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 // setupDatabaseListeners sets up database change listeners for real-time updates
func (h *Hub) setupDatabaseListeners() { func (h *Hub) setupDatabaseListeners() {
// Listen for changes on retribusi table // 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) h.handleDatabaseChange(channel, payload)
}) })
+146 -96
View File
@@ -4,17 +4,67 @@ global:
enable_swagger: true enable_swagger: true
enable_logging: 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: services:
retribusi: qris:
name: "Retribusi" name: "QRIS"
category: "retribusi" category: "qris"
package: "retribusi" package: "qris"
description: "Retribusi service for tariff and billing management" description: "QRIS service for QR code payment management"
base_url: "" base_url: ""
timeout: 30 timeout: 30
retry_count: 3 retry_count: 3
endpoints: 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: # retribusi:
# description: "Retribusi tariff management" # description: "Retribusi tariff management"
# handler_folder: "retribusi" # handler_folder: "retribusi"
@@ -152,99 +202,99 @@ services:
# has_stats: true # has_stats: true
# Example of another service # Example of another service
user: # user:
name: "User" # name: "User"
category: "user" # category: "user"
package: "user" # package: "user"
description: "User management service" # description: "User management service"
base_url: "" # base_url: ""
timeout: 30 # timeout: 30
retry_count: 3 # retry_count: 3
endpoints: # endpoints:
user: # user:
description: "User management endpoints" # description: "User management endpoints"
handler_folder: "retribusi" # handler_folder: "retribusi"
handler_file: "user.go" # handler_file: "user.go"
handler_name: "User" # handler_name: "User"
table_name: "data_user" # table_name: "data_user"
functions: # functions:
list: # list:
methods: ["GET"] # methods: ["GET"]
path: "/" # path: "/"
get_routes: "/" # get_routes: "/"
get_path: "/" # get_path: "/"
model: "User" # model: "User"
response_model: "UserGetResponse" # response_model: "UserGetResponse"
description: "Get user list with pagination" # description: "Get user list with pagination"
summary: "Get User List" # summary: "Get User List"
tags: ["User"] # tags: ["User"]
require_auth: true # require_auth: true
cache_enabled: true # cache_enabled: true
enable_database: true # enable_database: true
cache_ttl: 300 # cache_ttl: 300
has_pagination: true # has_pagination: true
has_filter: true # has_filter: true
has_search: true # has_search: true
get: # get:
methods: ["GET"] # methods: ["GET"]
path: "/:id" # path: "/:id"
get_routes: "/:id" # get_routes: "/:id"
get_path: "/:id" # get_path: "/:id"
model: "User" # model: "User"
response_model: "UserGetByIDResponse" # response_model: "UserGetByIDResponse"
description: "Get user by ID" # description: "Get user by ID"
summary: "Get User by ID" # summary: "Get User by ID"
tags: ["User"] # tags: ["User"]
require_auth: true # require_auth: true
cache_enabled: true # cache_enabled: true
enable_database: true # enable_database: true
cache_ttl: 300 # cache_ttl: 300
create: # create:
methods: ["POST"] # methods: ["POST"]
path: "/" # path: "/"
post_routes: "/" # post_routes: "/"
post_path: "/" # post_path: "/"
model: "UserCreateRequest" # model: "UserCreateRequest"
response_model: "UserCreateResponse" # response_model: "UserCreateResponse"
request_model: "UserCreateRequest" # request_model: "UserCreateRequest"
description: "Create new user" # description: "Create new user"
summary: "Create User" # summary: "Create User"
tags: ["User"] # tags: ["User"]
require_auth: true # require_auth: true
cache_enabled: false # cache_enabled: false
enable_database: true # enable_database: true
cache_ttl: 0 # cache_ttl: 0
update: # update:
methods: ["PUT"] # methods: ["PUT"]
path: "/:id" # path: "/:id"
put_routes: "/:id" # put_routes: "/:id"
put_path: "/:id" # put_path: "/:id"
model: "UserUpdateRequest" # model: "UserUpdateRequest"
response_model: "UserUpdateResponse" # response_model: "UserUpdateResponse"
request_model: "UserUpdateRequest" # request_model: "UserUpdateRequest"
description: "Update user" # description: "Update user"
summary: "Update User" # summary: "Update User"
tags: ["User"] # tags: ["User"]
require_auth: true # require_auth: true
cache_enabled: false # cache_enabled: false
enable_database: true # enable_database: true
cache_ttl: 0 # cache_ttl: 0
delete: # delete:
methods: ["DELETE"] # methods: ["DELETE"]
path: "/:id" # path: "/:id"
delete_routes: "/:id" # delete_routes: "/:id"
delete_path: "/:id" # delete_path: "/:id"
model: "User" # model: "User"
response_model: "UserDeleteResponse" # response_model: "UserDeleteResponse"
description: "Delete user" # description: "Delete user"
summary: "Delete User" # summary: "Delete User"
tags: ["User"] # tags: ["User"]
require_auth: true # require_auth: true
cache_enabled: false # cache_enabled: false
enable_database: true # enable_database: true
cache_ttl: 0 # cache_ttl: 0