first commit

This commit is contained in:
meninjar
2026-04-14 01:23:34 +00:00
commit edfaa886ff
443 changed files with 1245931 additions and 0 deletions
@@ -0,0 +1,38 @@
package antrol
import (
"net/http"
antrolService "service/internal/bpjs/antrol/reference"
"service/pkg/errors"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type AntrolHandler struct {
service antrolService.Service
}
func NewAntrolHandler(service antrolService.Service) *AntrolHandler {
return &AntrolHandler{service: service}
}
func (h *AntrolHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/bpjs/antrol/reference")
{
group.GET("/poli", h.GetRefPoli)
}
}
func (h *AntrolHandler) GetRefPoli(c *gin.Context) {
ctx := c.Request.Context()
res, err := h.service.GetRefPoli(ctx)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved BPJS Antrol Poli reference", res)
}
@@ -0,0 +1,100 @@
package aplicare
import (
"net/http"
"strconv"
bedService "service/internal/bpjs/aplicare/bed"
"service/pkg/errors"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type BedHandler struct {
service bedService.Service
}
func NewBedHandler(service bedService.Service) *BedHandler {
return &BedHandler{service: service}
}
func (h *BedHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/bpjs/aplicare/bed")
{
group.GET("/:kdppk/:start/:limit", h.GetBedList)
group.POST("/:kdppk", h.CreateBed)
group.PUT("/:kdppk", h.UpdateBed)
group.DELETE("/:kdppk", h.DeleteBed)
}
}
func (h *BedHandler) GetBedList(c *gin.Context) {
kdPpk := c.Param("kdppk")
start, _ := strconv.Atoi(c.Param("start"))
limit, _ := strconv.Atoi(c.Param("limit"))
ctx := c.Request.Context()
res, err := h.service.GetBedList(ctx, kdPpk, start, limit)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved BPJS Aplicares bed list", res)
}
func (h *BedHandler) CreateBed(c *gin.Context) {
kdPpk := c.Param("kdppk")
var req bedService.BedData
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
if err := h.service.CreateBed(ctx, kdPpk, req); err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusCreated, "Successfully created BPJS Aplicares bed data", nil)
}
func (h *BedHandler) UpdateBed(c *gin.Context) {
kdPpk := c.Param("kdppk")
var req bedService.BedData
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
if err := h.service.UpdateBed(ctx, kdPpk, req); err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully updated BPJS Aplicares bed data", nil)
}
func (h *BedHandler) DeleteBed(c *gin.Context) {
kdPpk := c.Param("kdppk")
var req bedService.BedDeletePayload
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
if err := h.service.DeleteBed(ctx, kdPpk, req); err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully deleted BPJS Aplicares bed data", nil)
}
@@ -0,0 +1,53 @@
package apotek
import (
"net/http"
dphoService "service/internal/bpjs/apotek/reference/dpho"
poliService "service/internal/bpjs/apotek/reference/poli"
"service/pkg/errors"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type ReferenceHandler struct {
dphoService dphoService.Service
poliService poliService.Service
}
func NewReferenceHandler(dpho dphoService.Service, poli poliService.Service) *ReferenceHandler {
return &ReferenceHandler{
dphoService: dpho,
poliService: poli,
}
}
func (h *ReferenceHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/bpjs/apotek/reference")
{
group.GET("/dpho", h.GetDPHO)
group.GET("/poli/:param", h.GetPoli)
}
}
func (h *ReferenceHandler) GetDPHO(c *gin.Context) {
res, err := h.dphoService.GetDPHO(c.Request.Context())
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved BPJS Apotek DPHO reference", res)
}
func (h *ReferenceHandler) GetPoli(c *gin.Context) {
param := c.Param("param")
res, err := h.poliService.GetPoli(c.Request.Context(), param)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved BPJS Apotek Poli reference", res)
}
@@ -0,0 +1,64 @@
package vclaim
import (
"net/http"
"time"
pesertaService "service/internal/bpjs/vclaim/peserta"
"service/pkg/errors"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type PesertaHandler struct {
service pesertaService.Service
}
func NewPesertaHandler(service pesertaService.Service) *PesertaHandler {
return &PesertaHandler{service: service}
}
func (h *PesertaHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/bpjs/vclaim/peserta")
{
group.GET("/nik/:nik", h.GetPesertaByNIK)
group.GET("/nokartu/:nokartu", h.GetPesertaByNoKartu)
}
}
func (h *PesertaHandler) GetPesertaByNIK(c *gin.Context) {
nik := c.Param("nik")
tglSEP := c.Query("tgl_sep")
if tglSEP == "" {
tglSEP = time.Now().Format("2006-01-02")
}
ctx := c.Request.Context()
res, err := h.service.GetPesertaByNIK(ctx, nik, tglSEP)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved participant data by NIK", res)
}
func (h *PesertaHandler) GetPesertaByNoKartu(c *gin.Context) {
noKartu := c.Param("nokartu")
tglSEP := c.Query("tgl_sep")
if tglSEP == "" {
tglSEP = time.Now().Format("2006-01-02")
}
ctx := c.Request.Context()
res, err := h.service.GetPesertaByNoKartu(ctx, noKartu, tglSEP)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved participant data by card number", res)
}
@@ -0,0 +1,105 @@
package vclaim
import (
"net/http"
sepService "service/internal/bpjs/vclaim/sep"
"service/pkg/errors"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
// SepHandler menangani semua request HTTP terkait VClaim.
type SepHandler struct {
sepService sepService.Service
}
// NewSepHandler membuat instance SepHandler baru.
func NewSepHandler(sepService sepService.Service) *SepHandler {
return &SepHandler{
sepService: sepService,
}
}
// RegisterRoutes mendaftarkan semua rute untuk BPJS VClaim.
func (h *SepHandler) RegisterRoutes(router *gin.RouterGroup) {
bpjsGroup := router.Group("/bpjs/vclaim")
{
sepGroup := bpjsGroup.Group("/sep")
{
sepGroup.POST("", h.CreateSEP)
sepGroup.PUT("", h.UpdateSEP)
// BPJS API untuk delete menggunakan method POST, tapi kita ekspos sebagai DELETE untuk konsistensi RESTful.
sepGroup.DELETE("", h.DeleteSEP)
sepGroup.GET("/:nosep", h.GetSEPDetail)
}
}
}
// CreateSEP menangani request untuk membuat SEP baru.
func (h *SepHandler) CreateSEP(c *gin.Context) {
var req sepService.CreateSEPRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
result, err := h.sepService.Create(c.Request.Context(), req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), nil)
return
}
response.Success(c, http.StatusCreated, "Successfully created SEP", result)
}
// UpdateSEP menangani request untuk memperbarui SEP.
func (h *SepHandler) UpdateSEP(c *gin.Context) {
var req sepService.UpdateSEPRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
result, err := h.sepService.Update(c.Request.Context(), req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), nil)
return
}
response.Success(c, http.StatusOK, "Successfully updated SEP", result)
}
// GetSEPDetail menangani request untuk mendapatkan detail SEP.
func (h *SepHandler) GetSEPDetail(c *gin.Context) {
noSEP := c.Param("no_sep")
result, err := h.sepService.GetDetail(c.Request.Context(), noSEP)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), nil)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved SEP detail", result)
}
// DeleteSEP menangani request untuk menghapus SEP.
func (h *SepHandler) DeleteSEP(c *gin.Context) {
var req sepService.DeleteSEPRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body for SEP deletion", err.Error())
return
}
result, err := h.sepService.Delete(c.Request.Context(), req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), nil)
return
}
response.Success(c, http.StatusOK, "Successfully deleted SEP", result)
}
@@ -0,0 +1,181 @@
package auth
import (
"net/http"
authService "service/internal/auth"
"service/pkg/errors"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
service authService.Service
}
func NewAuthHandler(service authService.Service) *AuthHandler {
return &AuthHandler{
service: service,
}
}
func (h *AuthHandler) RegisterRoutes(router *gin.RouterGroup) {
auth := router.Group("/auth")
{
auth.POST("/login", h.Login)
auth.POST("/register", h.Register)
auth.POST("/refresh", h.RefreshToken)
}
}
// RegisterProtectedRoutes mendaftarkan endpoint auth yang membutuhkan token
func (h *AuthHandler) RegisterProtectedRoutes(router *gin.RouterGroup) {
auth := router.Group("/auth")
{
auth.POST("/logout", h.Logout)
auth.GET("/info", h.TokenInfo)
}
}
// Login godoc
//
// @Summary Login user
// @Description Authenticate user and return JWT & Refresh tokens
// @Tags auth
// @Accept json
// @Produce json
// @Param request body authService.LoginRequest true "Login credentials"
// @Success 200 {object} response.Response
// @Router /auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var req authService.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
// Pemanggilan service secara by-value
resp, err := h.service.Login(c.Request.Context(), req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Login successful", resp)
}
// Register godoc
//
// @Summary Register user
// @Description Register a new user
// @Tags auth
// @Accept json
// @Produce json
// @Param request body authService.RegisterRequest true "Registration details"
// @Success 201 {object} response.Response
// @Router /auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
var req authService.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
resp, err := h.service.Register(c.Request.Context(), req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusCreated, "User registered successfully", resp)
}
// RefreshToken godoc
//
// @Summary Refresh Token
// @Description Refresh expired access token using refresh token
// @Tags auth
// @Accept json
// @Produce json
// @Param request body authService.RefreshTokenRequest true "Refresh token payload"
// @Success 200 {object} response.Response
// @Router /auth/refresh [post]
func (h *AuthHandler) RefreshToken(c *gin.Context) {
var req authService.RefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request payload", err.Error())
return
}
resp, err := h.service.RefreshToken(c.Request.Context(), req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Token refreshed successfully", resp)
}
// Logout godoc
//
// @Summary Logout user
// @Description Logout current user and blacklist the token
// @Tags auth
// @Produce json
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /auth/logout [post]
func (h *AuthHandler) Logout(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
response.Error(c, http.StatusBadRequest, "Token required", nil)
return
}
if len(token) > 7 && token[:7] == "Bearer " {
token = token[7:]
}
err := h.service.Logout(c.Request.Context(), token)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Logged out successfully", nil)
}
// TokenInfo godoc
//
// @Summary Get Token Info
// @Description Retrieve information about the currently active token/user
// @Tags auth
// @Produce json
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /auth/info [get]
func (h *AuthHandler) TokenInfo(c *gin.Context) {
// Data ini otomatis di-set oleh provider.go (UnifiedAuthMiddleware)
authProvider := c.GetString("auth_provider")
userID := c.GetString("user_id")
username := c.GetString("username")
email := c.GetString("email")
role := c.GetString("role")
name := c.GetString("name")
data := gin.H{
"auth_provider": authProvider, // Akan bernilai: "jwt", "keycloak", atau "static"
"user_id": userID,
"username": username,
"email": email,
"name": name,
"role": role,
}
response.Success(c, http.StatusOK, "Current active token information", data)
}
@@ -0,0 +1,470 @@
package health
import (
"context"
"fmt"
"net/http"
"time"
"service/internal/infrastructure/cache"
"service/internal/infrastructure/config"
"service/internal/infrastructure/database"
"service/internal/interfaces/minio"
"github.com/gin-gonic/gin"
miniogo "github.com/minio/minio-go/v7"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// HealthHandler handles health check endpoints
type HealthHandler struct {
db *gorm.DB
redisClient *redis.Client
config *config.Config
cacheManager *cache.Manager // Alternatif untuk Redis client
dbManager database.Service
}
// NewHealthHandlerWithCache creates a new health handler with cache manager
func NewHealthHandlerWithCache(db *gorm.DB, cacheManager *cache.Manager, config *config.Config, dbManager database.Service) *HealthHandler {
handler := &HealthHandler{
db: db,
config: config,
cacheManager: cacheManager,
dbManager: dbManager,
}
// Coba dapatkan Redis client dari cache manager
if cacheManager != nil {
if redisClientInterface := cacheManager.GetRedisClient(); redisClientInterface != nil {
if client, ok := redisClientInterface.(*redis.Client); ok {
handler.redisClient = client
}
}
}
return handler
}
// HealthCheckComplete performs comprehensive health check
func (h *HealthHandler) HealthCheckComplete(c *gin.Context) {
startTime := time.Now()
health := gin.H{
"status": "healthy",
"timestamp": time.Now().UTC(),
"uptime": time.Since(startTime).Milliseconds(),
"version": "1.0.0",
"service": "service",
}
// Check database
dbStatus := h.checkDatabase()
health["database"] = dbStatus
// Check cache
cacheStatus := h.checkCache()
health["cache"] = cacheStatus
// Check external services
externalStatus := h.checkExternalServices()
health["external_services"] = externalStatus
// Check Minio
minioStatus := h.checkMinio()
health["minio"] = minioStatus
// Determine overall status
if dbStatus["status"] != "UP" || cacheStatus["status"] != "UP" || (minioStatus["status"] != "UP" && minioStatus["status"] != "DISABLED") {
health["status"] = "DOWN"
c.JSON(http.StatusServiceUnavailable, health)
return
}
c.JSON(http.StatusOK, health)
}
// HealthCheckDatabase checks database connectivity
func (h *HealthHandler) HealthCheckDatabase(c *gin.Context) {
status := h.checkDatabase()
if status["status"] != "UP" {
c.JSON(http.StatusServiceUnavailable, status)
return
}
c.JSON(http.StatusOK, status)
}
// HealthCheckCache checks cache connectivity
func (h *HealthHandler) HealthCheckCache(c *gin.Context) {
status := h.checkCache()
if status["status"] != "UP" {
c.JSON(http.StatusServiceUnavailable, status)
return
}
c.JSON(http.StatusOK, status)
}
// HealthCheckExternal checks external service connectivity
func (h *HealthHandler) HealthCheckExternal(c *gin.Context) {
status := h.checkExternalServices()
// Hanya return 503 (Unavailable) jika status benar-benar DOWN, bukan saat sekadar DEGRADED.
if status["status"] == "DOWN" {
c.JSON(http.StatusServiceUnavailable, status)
return
}
c.JSON(http.StatusOK, status)
}
// HealthCheckMinio checks Minio Object Storage connectivity
func (h *HealthHandler) HealthCheckMinio(c *gin.Context) {
status := h.checkMinio()
if status["status"] == "DOWN" {
c.JSON(http.StatusServiceUnavailable, status)
return
}
c.JSON(http.StatusOK, status)
}
// TestUploadMinio is a testing endpoint to upload a file to Minio
func (h *HealthHandler) TestUploadMinio(c *gin.Context) {
if minio.I == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Minio client is not initialized or disconnected"})
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "File is required (form-data key: 'file')"})
return
}
// Gunakan bucket dari request body (jika ada), atau default ke config/nama statis
bucketName := c.DefaultPostForm("bucket", "dev-test")
ctx := c.Request.Context()
// Ensure bucket exists
exists, err := minio.I.BucketExists(ctx, bucketName)
if err == nil && !exists {
err = minio.I.MakeBucket(ctx, bucketName, miniogo.MakeBucketOptions{Region: h.config.Minio.Region})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()})
return
}
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check bucket status: " + err.Error()})
return
}
src, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open file: " + err.Error()})
return
}
defer src.Close()
objectName := fmt.Sprintf("%d-%s", time.Now().Unix(), file.Filename)
info, err := minio.I.PutObject(ctx, bucketName, objectName, src, file.Size, miniogo.PutObjectOptions{
ContentType: file.Header.Get("Content-Type"),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload to Minio: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "File uploaded successfully",
"data": info,
})
}
// checkDatabase checks database health
func (h *HealthHandler) checkDatabase() gin.H {
if h.dbManager == nil {
return gin.H{
"status": "UNKNOWN",
"error": "Database manager not initialized",
}
}
allDbInfo := h.dbManager.GetAllDatabasesInfo()
overallStatus := "UP"
// Iterate through the map and check each database
for name, info := range allDbInfo {
dbInfo, ok := info.(gin.H)
if !ok {
continue
}
// Standardize the status to "UP" if it's "connected" or "healthy"
if status, ok := dbInfo["status"].(string); ok {
if status == "connected" || status == "healthy" || status == "UP" {
dbInfo["status"] = "UP"
} else {
overallStatus = "DOWN"
}
}
allDbInfo[name] = dbInfo // Update the map with the checked info
}
return gin.H{
"status": overallStatus,
"components": allDbInfo,
}
}
// checkCache checks Redis/cache health
func (h *HealthHandler) checkCache() gin.H {
if h.cacheManager == nil {
return gin.H{"status": "UNKNOWN", "error": "Cache manager not initialized"}
}
if !h.config.Cache.Enabled {
return gin.H{"status": "UP", "details": gin.H{"provider": "noop", "message": "Cache is disabled"}}
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
start := time.Now()
err := h.cacheManager.Health(ctx)
latency := time.Since(start)
details := gin.H{
"provider": "redis",
"latency": latency.String(),
"latency_ms": latency.Milliseconds(),
}
if err != nil {
details["error"] = err.Error()
return gin.H{"status": "DOWN", "details": details}
}
return gin.H{
"status": "UP",
"details": details,
}
}
// checkMinio checks Minio Object Storage health
func (h *HealthHandler) checkMinio() gin.H {
if h.config == nil || h.config.Minio.Endpoint == "" {
return gin.H{"status": "DISABLED", "message": "Minio is not configured in .env"}
}
if minio.I == nil {
return gin.H{"status": "DOWN", "message": "Minio global client is nil"}
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
start := time.Now()
// Panggilan ringan API minio untuk memastikan server hidup & kredensial benar
_, err := minio.I.ListBuckets(ctx)
latency := time.Since(start)
details := gin.H{
"endpoint": h.config.Minio.Endpoint,
"latency_ms": latency.Milliseconds(),
}
if err != nil {
details["error"] = err.Error()
return gin.H{"status": "DOWN", "details": details}
}
return gin.H{"status": "UP", "details": details}
}
// checkExternalServices checks external service health
func (h *HealthHandler) checkExternalServices() gin.H {
status := gin.H{
"status": "UP",
"message": "All configured external services are reachable",
}
hasDegraded := false
hasDown := false
downServices := []string{}
degradedServices := []string{}
activeServices := 0
// Check BPJS service if configured
if h.config != nil {
bpjsStatus := h.checkBPJSService()
status["bpjs"] = bpjsStatus
if s, ok := bpjsStatus["status"].(string); ok && s != "DISABLED" {
activeServices++
if s == "DOWN" {
hasDown = true
downServices = append(downServices, "BPJS")
} else if s == "DEGRADED" {
hasDegraded = true
degradedServices = append(degradedServices, "BPJS")
}
}
}
// Check SatuSehat service if configured
if h.config != nil {
satuSehatStatus := h.checkSatuSehatService()
status["satu_sehat"] = satuSehatStatus
if s, ok := satuSehatStatus["status"].(string); ok && s != "DISABLED" {
activeServices++
if s == "DOWN" {
hasDown = true
downServices = append(downServices, "SatuSehat")
} else if s == "DEGRADED" {
hasDegraded = true
degradedServices = append(degradedServices, "SatuSehat")
}
}
}
if activeServices == 0 {
status["status"] = "UP"
status["message"] = "All external services are disabled"
} else if hasDown {
status["status"] = "DOWN"
status["message"] = fmt.Sprintf("Some external services are unreachable: %v", downServices)
} else if hasDegraded {
status["status"] = "DEGRADED"
status["message"] = fmt.Sprintf("Some external services are degraded: %v", degradedServices)
}
return status
}
// checkBPJSService checks BPJS VClaim service
func (h *HealthHandler) checkBPJSService() gin.H {
if !h.config.Bpjs.Enabled {
return gin.H{
"status": "DISABLED",
"message": "BPJS integration is disabled in configuration",
}
}
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequest("GET", h.config.Bpjs.BaseURL, nil)
if err != nil {
return gin.H{
"status": "DOWN",
"message": "Failed to create BPJS request: " + err.Error(),
}
}
resp, err := client.Do(req)
if err != nil {
return gin.H{
"status": "DOWN",
"message": "BPJS service unreachable: " + err.Error(),
}
}
defer resp.Body.Close()
status := "UP"
message := "BPJS service is reachable"
switch resp.StatusCode {
case http.StatusOK:
message = "BPJS service is reachable and responding normally"
case http.StatusUnauthorized:
message = "BPJS service is reachable (Authentication required/Invalid Signature)"
case http.StatusForbidden:
status = "DEGRADED"
message = "BPJS service is reachable, but access is forbidden (Check IP Whitelisting or Credentials)"
case http.StatusNotFound:
message = "BPJS service is reachable (Base URL responded with 404, server is UP)"
default:
if resp.StatusCode >= 500 {
status = "DOWN"
message = fmt.Sprintf("BPJS service returned server error: %s", resp.Status)
} else {
message = fmt.Sprintf("BPJS service is reachable (Status: %s)", resp.Status)
}
}
return gin.H{
"status": status,
"message": message,
"status_code": resp.StatusCode,
}
}
// checkSatuSehatService checks SatuSehat FHIR service
func (h *HealthHandler) checkSatuSehatService() gin.H {
if !h.config.SatuSehat.Enabled {
return gin.H{
"status": "DISABLED",
"message": "SatuSehat integration is disabled in configuration",
}
}
client := &http.Client{
Timeout: 2 * time.Second, // Dipercepat agar health check tidak blocking terlalu lama
}
checkURL := func(url string) (string, string, int) {
if url == "" {
return "DISABLED", "URL not configured", 0
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "DOWN", "Request creation failed: " + err.Error(), 0
}
resp, err := client.Do(req)
if err != nil {
return "DOWN", "Unreachable: " + err.Error(), 0
}
defer resp.Body.Close()
return "UP", fmt.Sprintf("Reachable (Status: %s)", resp.Status), resp.StatusCode
}
authStatus, authMsg, authCode := checkURL(h.config.SatuSehat.AuthURL)
baseStatus, baseMsg, baseCode := checkURL(h.config.SatuSehat.BaseURL)
consentStatus, consentMsg, _ := checkURL(h.config.SatuSehat.ConsentURL)
kfaStatus, kfaMsg, _ := checkURL(h.config.SatuSehat.KFAURL)
overallStatus := "UP"
overallMessage := "SatuSehat services are reachable"
// Evaluasi status keseluruhan berdasarkan Base URL & Auth URL
if baseStatus == "DOWN" || authStatus == "DOWN" {
overallStatus = "DOWN"
overallMessage = "One or more critical SatuSehat services are unreachable"
} else if baseCode == http.StatusForbidden || authCode == http.StatusForbidden {
overallStatus = "DEGRADED"
overallMessage = "Services are reachable, but access is forbidden (Check IP/Permissions)"
}
return gin.H{
"status": overallStatus,
"message": overallMessage,
"details": gin.H{
"org_id": h.config.SatuSehat.OrgID,
"fasyankes_id": h.config.SatuSehat.FasyakesID,
"endpoints": gin.H{
"auth": gin.H{"url": h.config.SatuSehat.AuthURL, "status": authStatus, "message": authMsg, "status_code": authCode},
"fhir_base": gin.H{"url": h.config.SatuSehat.BaseURL, "status": baseStatus, "message": baseMsg, "status_code": baseCode},
"consent": gin.H{"url": h.config.SatuSehat.ConsentURL, "status": consentStatus, "message": consentMsg},
"kfa": gin.H{"url": h.config.SatuSehat.KFAURL, "status": kfaStatus, "message": kfaMsg},
},
},
}
}
@@ -0,0 +1,318 @@
package role
import (
"fmt"
"math"
"net/http"
"strconv"
"strings"
masterService "service/internal/master/role/master"
"service/pkg/errors"
"service/pkg/logger"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type RoleMasterHandler struct {
service masterService.Service
}
func NewRoleMasterHandler(service masterService.Service) *RoleMasterHandler {
return &RoleMasterHandler{service: service}
}
func (h *RoleMasterHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/roles/master")
{
group.GET("", h.GetList)
group.GET("/search", h.Search)
group.GET("/:id", h.GetDetail)
group.POST("", h.Create)
group.PUT("/:id", h.Update)
group.DELETE("/:id", h.Delete)
}
}
// GetList godoc
//
// @Summary Get list of Role
// @Description Retrieve a paginated list of RoleMaster
// @Tags roles
// @Produce json
// @Param limit query int false "Number of items per page" default(10)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sort query string false "Sort fields (e.g. +name,-created_at)"
// @Param active query boolean false "Filter by active status"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/master [get]
func (h *RoleMasterHandler) GetList(c *gin.Context) {
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limitStr := c.Query("limit"); limitStr != "" {
limit, _ = strconv.Atoi(limitStr)
}
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil && offset > 0 && limit > 0 {
offset = (offset - 1) * limit
}
}
activeParam := c.Query("active")
var activeFilter *bool // default nil (no filter / ambil semua)
if activeParam != "" {
isActive := activeParam == "true" || activeParam == "1"
activeFilter = &isActive
}
// Parse parameter sort (format: sort=column1,-column2,+column3)
// -column untuk DESC, +column atau column untuk ASC
var sorts []string
if sortParam := c.Query("sort"); sortParam != "" {
sorts = strings.Split(sortParam, ",")
// Validasi dan bersihkan sort parameters
for i, sort := range sorts {
sorts[i] = strings.TrimSpace(sort)
}
}
ctx := c.Request.Context()
result, err := h.service.GetList(ctx, limit, offset, sorts, activeFilter)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
data := result["data"]
total := result["total"].(int64)
limitVal := result["limit"].(int)
offsetVal := result["offset"].(int)
page := 1
if limitVal > 0 {
page = (offsetVal / limitVal) + 1
}
totalPages := 0
if limitVal > 0 {
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
}
meta := response.Meta{Page: page, Limit: limitVal, Total: int(total), TotalPages: totalPages}
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPages list", data, meta)
}
// GetDetail godoc
//
// @Summary Get RoleMaster detail
// @Description Retrieve detailed information about a specific RoleMaster
// @Tags roles
// @Produce json
// @Param id path int true "RoleMaster ID"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/master/{id} [get]
func (h *RoleMasterHandler) GetDetail(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
ctx := c.Request.Context()
result, err := h.service.GetDetail(ctx, id)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved RoleMaster detail", result)
}
// Search godoc
//
// @Summary Search Role
// @Description Search RoleMaster records using dynamic filters
// @Tags roles
// @Produce json
// @Param limit query int false "Limit per page" default(10)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sort query string false "Sort fields (e.g. +name,-created_at)"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/master/search [get]
func (h *RoleMasterHandler) Search(c *gin.Context) {
// Parse Limit & Offset dengan fallback ke page & page_size
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limitStr := c.Query("limit"); limitStr != "" {
limit, _ = strconv.Atoi(limitStr)
}
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil && offset > 0 && limit > 0 {
offset = (offset - 1) * limit
}
}
// Ambil parameter filter
// Ambil parameter filter secara dinamis
filters := make(map[string]interface{})
for key, values := range c.Request.URL.Query() {
if key == "page" || key == "limit" || key == "page_size" || key == "offset" || key == "sort" {
continue
}
if len(values) > 0 && values[0] != "" {
filters[key] = values[0]
}
}
// Parse parameter sort (format: sort=column1,-column2,+column3)
// -column untuk DESC, +column atau column untuk ASC
var sorts []string
if sortParam := c.Query("sort"); sortParam != "" {
sorts = strings.Split(sortParam, ",")
// Validasi dan bersihkan sort parameters
for i, sort := range sorts {
sorts[i] = strings.TrimSpace(sort)
}
}
ctx := c.Request.Context()
logger.Default().WithContext(ctx).Info("Search request",
logger.String("filters", fmt.Sprintf("%v", filters)),
logger.String("sorts", fmt.Sprintf("%v", sorts)),
logger.Int("offset", offset),
logger.Int("limit", limit))
// Panggil service dengan parameter sort tambahan
result, err := h.service.Search(ctx, filters, sorts, limit, offset)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
// Extract data dari map service untuk response format
data := result["data"]
total := result["total"].(int64)
limitVal := result["limit"].(int)
offsetVal := result["offset"].(int)
page := 1
if limitVal > 0 {
page = (offsetVal / limitVal) + 1
}
// Hitung total pages
totalPages := 0
if limitVal > 0 {
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
}
meta := response.Meta{
Page: page,
Limit: limitVal,
Total: int(total),
TotalPages: totalPages,
}
response.Paginated(c, http.StatusOK, "Successfully retrieved RoleMaster search results", data, meta)
}
// Create godoc
//
// @Summary Create new RoleMaster
// @Description Create a new RoleMaster record
// @Tags roles
// @Accept json
// @Produce json
// @Param request body masterService.RoleMasterRequest true "Payload"
// @Success 201 {object} response.Response
// @Security BearerAuth
// @Router /roles/master [post]
func (h *RoleMasterHandler) Create(c *gin.Context) {
var req masterService.RoleMasterRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
created, err := h.service.Create(ctx, req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusCreated, "Successfully created RoleMaster", created)
}
// Update godoc
//
// @Summary Update an existing RoleMaster
// @Description Update details of an existing RoleMaster record by ID
// @Tags roles
// @Accept json
// @Produce json
// @Param id path int true "RoleMaster ID"
// @Param request body masterService.RoleMasterRequest true "Payload"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/master/{id} [put]
func (h *RoleMasterHandler) Update(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
var req masterService.RoleMasterRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
updated, err := h.service.Update(ctx, id, req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully updated RoleMaster", updated)
}
// Delete godoc
//
// @Summary Delete a RoleMaster
// @Description Delete a RoleMaster record by ID (soft delete)
// @Tags roles
// @Produce json
// @Param id path int true "RoleMaster ID"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/master/{id} [delete]
func (h *RoleMasterHandler) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
ctx := c.Request.Context()
if err := h.service.Delete(ctx, id); err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully deleted RoleMaster", nil)
}
@@ -0,0 +1,384 @@
package role
import (
"fmt"
"math"
"net/http"
"strconv"
"strings"
roleService "service/internal/master/role/pages"
"service/pkg/errors"
"service/pkg/logger"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type RolPagesHandler struct {
service roleService.Service
}
func NewRolPagesHandler(service roleService.Service) *RolPagesHandler {
return &RolPagesHandler{service: service}
}
func (h *RolPagesHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/roles/pages")
{
group.GET("", h.GetList)
group.GET("/tree", h.GetTree)
group.GET("/tree/level/:level", h.GetTreeByLevel)
group.GET("/search", h.Search)
group.GET("/:id", h.GetDetail)
group.POST("", h.Create)
group.PUT("/:id", h.Update)
group.DELETE("/:id", h.Delete)
}
}
// GetList godoc
//
// @Summary Get list of Role Pages
// @Description Retrieve a paginated list of Role Pages
// @Tags roles-pages
// @Produce json
// @Param limit query int false "Number of items per page" default(10)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sort query string false "Sort fields (e.g. +name,-created_at)"
// @Param active query boolean false "Filter by active status"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/pages [get]
func (h *RolPagesHandler) GetList(c *gin.Context) {
// Parse Limit & Offset dengan fallback ke page & page_size
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limitStr := c.Query("limit"); limitStr != "" {
limit, _ = strconv.Atoi(limitStr)
}
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil && offset > 0 && limit > 0 {
offset = (offset - 1) * limit
}
}
activeParam := c.Query("active")
var activeFilter *bool // default nil (no filter / ambil semua)
if activeParam != "" {
isActive := activeParam == "true" || activeParam == "1"
activeFilter = &isActive
}
// Parse parameter sort (format: sort=column1,-column2,+column3)
// -column untuk DESC, +column atau column untuk ASC
var sorts []string
if sortParam := c.Query("sort"); sortParam != "" {
sorts = strings.Split(sortParam, ",")
// Validasi dan bersihkan sort parameters
for i, sort := range sorts {
sorts[i] = strings.TrimSpace(sort)
}
}
ctx := c.Request.Context()
result, err := h.service.GetList(ctx, limit, offset, sorts, activeFilter)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
data := result["data"]
total := result["total"].(int64)
limitVal := result["limit"].(int)
offsetVal := result["offset"].(int)
page := 1
if limitVal > 0 {
page = (offsetVal / limitVal) + 1
}
totalPages := 0
if limitVal > 0 {
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
}
meta := response.Meta{Page: page, Limit: limitVal, Total: int(total), TotalPages: totalPages}
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPages list", data, meta)
}
// GetDetail godoc
//
// @Summary Get Role Pages detail
// @Description Retrieve detailed information about a specific Role Pages
// @Tags roles-pages
// @Produce json
// @Param id path int true "Role Pages ID"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/pages/{id} [get]
func (h *RolPagesHandler) GetDetail(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
ctx := c.Request.Context()
result, err := h.service.GetDetail(ctx, id)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved RolPages detail", result)
}
// Search godoc
//
// @Summary Search Role Pages
// @Description Search Role Pages records using dynamic filters
// @Tags roles-pages
// @Produce json
// @Param limit query int false "Limit per page" default(10)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sort query string false "Sort fields"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/pages/search [get]
func (h *RolPagesHandler) Search(c *gin.Context) {
// Parse Limit & Offset dengan fallback ke page & page_size
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limitStr := c.Query("limit"); limitStr != "" {
limit, _ = strconv.Atoi(limitStr)
}
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil && offset > 0 && limit > 0 {
offset = (offset - 1) * limit
}
}
// Ambil parameter filter secara dinamis
filters := make(map[string]interface{})
for key, values := range c.Request.URL.Query() {
if key == "page" || key == "limit" || key == "page_size" || key == "offset" || key == "sort" {
continue
}
if len(values) > 0 && values[0] != "" {
filters[key] = values[0]
}
}
// Parse parameter sort (format: sort=column1,-column2,+column3)
// -column untuk DESC, +column atau column untuk ASC
var sorts []string
if sortParam := c.Query("sort"); sortParam != "" {
sorts = strings.Split(sortParam, ",")
// Validasi dan bersihkan sort parameters
for i, sort := range sorts {
sorts[i] = strings.TrimSpace(sort)
}
}
ctx := c.Request.Context()
logger.Default().WithContext(ctx).Info("Search request",
logger.String("filters", fmt.Sprintf("%v", filters)),
logger.String("sorts", fmt.Sprintf("%v", sorts)),
logger.Int("offset", offset),
logger.Int("limit", limit))
// Panggil service dengan parameter sort tambahan
result, err := h.service.Search(ctx, filters, sorts, limit, offset)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
// Extract data dari map service untuk response format
data := result["data"]
total := result["total"].(int64)
limitVal := result["limit"].(int)
offsetVal := result["offset"].(int)
page := 1
if limitVal > 0 {
page = (offsetVal / limitVal) + 1
}
totalPages := 0
if limitVal > 0 {
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
}
meta := response.Meta{
Page: page,
Limit: limitVal,
Total: int(total),
TotalPages: totalPages,
}
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPages search results", data, meta)
}
// Create godoc
//
// @Summary Create new Role Pages
// @Description Create a new Role Pages record
// @Tags roles-pages
// @Accept json
// @Produce json
// @Param request body roleService.RolPagesRequest true "Payload"
// @Success 201 {object} response.Response
// @Security BearerAuth
// @Router /roles/pages [post]
func (h *RolPagesHandler) Create(c *gin.Context) {
var req roleService.RolPagesRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
created, err := h.service.Create(ctx, req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusCreated, "Successfully created RolPages", created)
}
// Update godoc
//
// @Summary Update an existing Role Pages
// @Description Update details of an existing Role Pages record by ID
// @Tags roles-pages
// @Accept json
// @Produce json
// @Param id path int true "Role Pages ID"
// @Param request body roleService.RolPagesRequest true "Payload"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/pages/{id} [put]
func (h *RolPagesHandler) Update(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
var req roleService.RolPagesRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
updated, err := h.service.Update(ctx, id, req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully updated RolPages", updated)
}
// Delete godoc
//
// @Summary Delete a Role Pages
// @Description Delete a Role Pages record by ID
// @Tags roles-pages
// @Produce json
// @Param id path int true "Role Pages ID"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/pages/{id} [delete]
func (h *RolPagesHandler) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
ctx := c.Request.Context()
if err := h.service.Delete(ctx, id); err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully deleted RolPages", nil)
}
// GetTree godoc
//
// @Summary Get tree of Role Pages
// @Description Retrieve a hierarchical tree of Role Pages
// @Tags roles-pages
// @Produce json
// @Param active query boolean false "Filter by active status"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/pages/tree [get]
func (h *RolPagesHandler) GetTree(c *gin.Context) {
activeParam := c.Query("active")
var activeFilter *bool // default nil (no filter / ambil semua)
if activeParam != "" {
isActive := activeParam == "true" || activeParam == "1"
activeFilter = &isActive
}
ctx := c.Request.Context()
tree, err := h.service.GetTree(ctx, activeFilter)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved RolPages", tree)
}
// GetTreeByLevel godoc
//
// @Summary Get tree of Role Pages by level
// @Description Retrieve a hierarchical tree of Role Pages up to a specific level
// @Tags roles-pages
// @Produce json
// @Param level path int true "Hierarchy Level"
// @Param active query boolean false "Filter by active status"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/pages/tree/level/{level} [get]
func (h *RolPagesHandler) GetTreeByLevel(c *gin.Context) {
level, err := strconv.ParseInt(c.Param("level"), 10, 16)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid level format", nil)
return
}
activeParam := c.Query("active")
var activeFilter *bool // default nil (no filter / ambil semua)
if activeParam != "" {
isActive := activeParam == "true" || activeParam == "1"
activeFilter = &isActive
}
ctx := c.Request.Context()
tree, err := h.service.GetTreeByLevel(ctx, int16(level), activeFilter)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved RolPages", tree)
}
@@ -0,0 +1,502 @@
package role
import (
"fmt"
"math"
"net/http"
"strconv"
"strings"
roleService "service/internal/master/role/permission"
"service/pkg/errors"
"service/pkg/logger"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type RolPermissionHandler struct {
service roleService.Service
}
func NewRolPermissionHandler(service roleService.Service) *RolPermissionHandler {
return &RolPermissionHandler{service: service}
}
func (h *RolPermissionHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/roles/permissions") // get by role
{
group.GET("", h.GetList)
group.GET("/search", h.Search)
group.GET("/:id", h.GetDetail) // using role id
group.GET("/role/:role", h.GetPermissionTreeRole) // get permission tree by role keycloak
group.POST("", h.Create)
group.PUT("/:id", h.Update)
group.DELETE("/:id", h.Delete)
group.GET("/rolemaster/:id", h.GetRolePagesList)
}
}
// GetList godoc
//
// @Summary Get list of Role Permissions
// @Description Retrieve a paginated list of Role Permissions
// @Tags roles-permissions
// @Produce json
// @Param limit query int false "Number of items per page" default(10)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sort query string false "Sort fields (e.g. +name,-created_at)"
// @Param active query boolean false "Filter by active status"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/permissions [get]
func (h *RolPermissionHandler) GetList(c *gin.Context) {
// Parse Limit & Offset dengan fallback ke page & page_size
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if pageSizeStr := c.Query("limit"); pageSizeStr != "" {
limit, _ = strconv.Atoi(pageSizeStr)
}
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
if pageStr := c.Query("offset"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 && limit > 0 {
offset = (page - 1) * limit
}
}
activeParam := c.Query("active")
var activeFilter *bool // default nil (no filter / ambil semua)
if activeParam != "" {
isActive := activeParam == "true" || activeParam == "1"
activeFilter = &isActive
}
// Parse parameter sort (format: sort=column1,-column2,+column3)
// -column untuk DESC, +column atau column untuk ASC
var sorts []string
if sortParam := c.Query("sort"); sortParam != "" {
sorts = strings.Split(sortParam, ",")
// Validasi dan bersihkan sort parameters
for i, sort := range sorts {
sorts[i] = strings.TrimSpace(sort)
}
}
ctx := c.Request.Context()
result, err := h.service.GetList(ctx, limit, offset, sorts, activeFilter)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
data := result["data"]
total := result["total"].(int64)
limitVal := result["limit"].(int)
offsetVal := result["offset"].(int)
page := 1
if limitVal > 0 {
page = (offsetVal / limitVal) + 1
}
totalPages := 0
if limitVal > 0 {
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
}
meta := response.Meta{Page: page, Limit: limitVal, Total: int(total), TotalPages: totalPages}
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPermission list", data, meta)
}
// GetDetail godoc
//
// @Summary Get Role Permission detail
// @Description Retrieve detailed information about a specific Role Permission
// @Tags roles-permissions
// @Produce json
// @Param id path int true "Role Permission ID"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/permissions/{id} [get]
func (h *RolPermissionHandler) GetDetail(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
ctx := c.Request.Context()
result, err := h.service.GetDetail(ctx, id)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully retrieved RolPermission detail", result)
}
// Search godoc
//
// @Summary Search Role Permissions
// @Description Search Role Permissions records using dynamic filters
// @Tags roles-permissions
// @Produce json
// @Param limit query int false "Limit per page" default(10)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sort query string false "Sort fields"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/permissions/search [get]
func (h *RolPermissionHandler) Search(c *gin.Context) {
// Parse Limit & Offset dengan fallback ke page & page_size
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if pageSizeStr := c.Query("limit"); pageSizeStr != "" {
limit, _ = strconv.Atoi(pageSizeStr)
}
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
if pageStr := c.Query("offset"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 && limit > 0 {
offset = (page - 1) * limit
}
}
// Ambil parameter filter secara dinamis
filters := make(map[string]interface{})
for key, values := range c.Request.URL.Query() {
if key == "page" || key == "limit" || key == "page_size" || key == "offset" || key == "sort" {
continue
}
if len(values) > 0 && values[0] != "" {
filters[key] = values[0]
}
}
// Parse parameter sort (format: sort=column1,-column2,+column3)
// -column untuk DESC, +column atau column untuk ASC
var sorts []string
if sortParam := c.Query("sort"); sortParam != "" {
sorts = strings.Split(sortParam, ",")
// Validasi dan bersihkan sort parameters
for i, sort := range sorts {
sorts[i] = strings.TrimSpace(sort)
}
}
ctx := c.Request.Context()
logger.Default().WithContext(ctx).Info("Search request",
logger.String("filters", fmt.Sprintf("%v", filters)),
logger.String("sorts", fmt.Sprintf("%v", sorts)),
logger.Int("offset", offset),
logger.Int("limit", limit))
// Panggil service dengan parameter sort tambahan
result, err := h.service.Search(ctx, filters, sorts, limit, offset)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
// Extract data dari map service untuk response format
data := result["data"]
total := result["total"].(int64)
limitVal := result["limit"].(int)
offsetVal := result["offset"].(int)
page := 1
if limitVal > 0 {
page = (offsetVal / limitVal) + 1
}
totalPages := 0
if limitVal > 0 {
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
}
meta := response.Meta{
Page: page,
Limit: limitVal,
Total: int(total),
TotalPages: totalPages,
}
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPermission search results", data, meta)
}
// GetPermissionTree godoc
//
// @Summary Get Permission Tree
// @Description Retrieve a permission tree
// @Tags roles-permissions
// @Produce json
// @Param role path string true "Role Keycloak"
// @Param groups query string false "Comma-separated groups"
// @Param active query boolean false "Filter by active status"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/permissions/tree/{role} [get]
func (h *RolPermissionHandler) GetPermissionTree(c *gin.Context) {
roleKeycloak := c.Param("role")
// Get groups from query parameter (comma-separated)
groupsParam := c.Query("groups")
var groups []string
if groupsParam != "" {
groups = strings.Split(groupsParam, ",")
// Trim spaces from each group
for i, group := range groups {
groups[i] = strings.TrimSpace(group)
}
}
// Parse active parameter - handle "true", "false", "1", "0"
activeParam := c.Query("active")
var activeOnly *bool // default nil (no filter)
if activeParam != "" {
isActive := activeParam == "true" || activeParam == "1"
activeOnly = &isActive
}
ctx := c.Request.Context()
result, err := h.service.GetRolePermissionTree(ctx, roleKeycloak, groups, activeOnly)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
// Jika role disediakan tapi tidak ada data, return empty result
if roleKeycloak != "" && (result == nil || len(result.Data.Access) == 0) {
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "No permissions found for the specified role",
"data": []interface{}{},
})
return
}
// Return the result in the desired format
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Successfully retrieved RolPages",
"data": result.Data,
})
}
// GetPermissionTreeRole godoc
//
// @Summary Get Permission Tree by Role
// @Description Retrieve a permission tree by Keycloak role
// @Tags roles-permissions
// @Produce json
// @Param role path string true "Role Keycloak"
// @Param active query boolean false "Filter by active status"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/permissions/role/{role} [get]
func (h *RolPermissionHandler) GetPermissionTreeRole(c *gin.Context) {
roleKeycloak := c.Param("role")
// Parse active parameter - handle "true", "false", "1", "0"
activeParam := c.Query("active")
var activeOnly *bool // default nil (no filter)
if activeParam != "" {
isActive := activeParam == "true" || activeParam == "1"
activeOnly = &isActive
}
ctx := c.Request.Context()
result, err := h.service.GetRolePermissionTreeRole(ctx, roleKeycloak, activeOnly)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
// Jika role disediakan tapi tidak ada data, return empty result
if roleKeycloak != "" && (result == nil || len(result.Data.Access) == 0) {
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "No permissions found for the specified role",
"data": []interface{}{},
})
return
}
// Return the result in the desired format
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Successfully retrieved RolPages",
"data": result.Data,
})
}
// Create godoc
//
// @Summary Create new Role Permission
// @Description Create a new Role Permission record
// @Tags roles-permissions
// @Accept json
// @Produce json
// @Param request body roleService.RolPermissionRequest true "Payload"
// @Success 201 {object} response.Response
// @Security BearerAuth
// @Router /roles/permissions [post]
func (h *RolPermissionHandler) Create(c *gin.Context) {
var req roleService.RolPermissionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
created, err := h.service.Create(ctx, req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusCreated, "Successfully created RolPermission", created)
}
// Update godoc
//
// @Summary Update an existing Role Permission
// @Description Update details of an existing Role Permission record by ID
// @Tags roles-permissions
// @Accept json
// @Produce json
// @Param id path int true "Role Permission ID"
// @Param request body roleService.RolPermissionRequest true "Payload"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/permissions/{id} [put]
func (h *RolPermissionHandler) Update(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
var req roleService.RolPermissionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
ctx := c.Request.Context()
updated, err := h.service.Update(ctx, id, req)
if err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully updated RolPermission", updated)
}
// Delete godoc
//
// @Summary Delete a Role Permission
// @Description Delete a Role Permission record by ID
// @Tags roles-permissions
// @Produce json
// @Param id path int true "Role Permission ID"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /roles/permissions/{id} [delete]
func (h *RolPermissionHandler) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
return
}
ctx := c.Request.Context()
if err := h.service.Delete(ctx, id); err != nil {
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
return
}
response.Success(c, http.StatusOK, "Successfully deleted RolPermission", nil)
}
// GetRolePagesList mengambil daftar role beserta page dan permission-nya
// @Summary Get Role Pages Access List
// @Description Retrieve a paginated list of RolePages grouped by Role with Tree Hierarchy
// @Tags roles-permissions
// @Produce json
// @Param id path string true "Role Master ID"
// @Param page query integer false "Page number" default(1)
// @Param limit query integer false "Items per page" default(10)
// @Param active query boolean false "Filter by active status"
// @Security BearerAuth
// @Success 200 {object} response.Response
// @Router /roles/permissions/rolemaster/{id} [get]
func (h *RolPermissionHandler) GetRolePagesList(c *gin.Context) {
ctx := c.Request.Context()
roleID := c.Param("id")
// 1. Ambil Parameter Pagination
pageStr := c.DefaultQuery("page", "1")
limitStr := c.DefaultQuery("limit", "10")
page, err := strconv.Atoi(pageStr)
if err != nil || page < 1 {
page = 1
}
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < -1 {
limit = 10
}
offset := (page - 1) * limit
if limit == -1 {
offset = 0 // Jika limit -1, fetch all data
}
// 2. Ambil Parameter Filter Active
defaultActive := true
activeFilter := &defaultActive
if activeStr := c.Query("active"); activeStr != "" {
parsedActive, err := strconv.ParseBool(activeStr)
if err == nil {
activeFilter = &parsedActive
}
}
// 3. Panggil Service layer
res, err := h.service.GetRolePagesList(ctx, roleID, limit, offset, activeFilter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": err.Error(),
})
return
}
// 4. Kalkulasi Metadata & Response Sesuai Format
total := res["total"].(int64)
totalPages := 1
if limit > 0 {
totalPages = int((total + int64(limit) - 1) / int64(limit))
}
// Format JSON Response
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Successfully retrieved RolPages list",
"data": res["data"],
"meta": gin.H{
"page": page,
"limit": limit,
"total": total,
"total_pages": totalPages,
},
})
}
@@ -0,0 +1,90 @@
package reference
import (
stdErrors "errors"
"net/http"
"service/internal/interfaces/satusehat"
"service/internal/satusehat/reference/auth"
"service/pkg/errors"
"service/pkg/response"
"time"
"github.com/gin-gonic/gin"
)
// AuthHandler menangani endpoint HTTP untuk Auth Satu Sehat.
type AuthHandler struct {
service auth.Service
}
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
func (h *AuthHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/reference")
{
group.GET("/auth/token", h.GetToken)
group.POST("/auth/token/refresh", h.RefreshToken)
}
}
// NewAuthHandler membuat instance baru dari AuthHandler.
func NewAuthHandler(service auth.Service) *AuthHandler {
return &AuthHandler{
service: service,
}
}
// handleSatuSehatError mengekstrak OperationOutcome agar JSON bisa tampil terstruktur di response
func handleSatuSehatError(c *gin.Context, err error) {
var ssErr *satusehat.ErrorOperationOutcome
if stdErrors.As(err, &ssErr) {
c.JSON(ssErr.StatusCode, gin.H{
"status": "error",
"message": ssErr.Outcome,
"error": gin.H{
"severity": "error",
"timestamp": time.Now().UTC().Format(time.RFC3339),
},
})
return
}
appErr := errors.FromError(err)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
}
// GetToken godoc
//
// @Summary Get SatuSehat Access Token
// @Description Mendapatkan token aktif untuk API Satu Sehat Kemenkes (menggunakan cache internal)
// @Tags Satu Sehat - Auth
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /satusehat/reference/auth/token [get]
// @Security BearerAuth
func (h *AuthHandler) GetToken(c *gin.Context) {
data, err := h.service.GetToken(c.Request.Context())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan token Satu Sehat", data)
}
// RefreshToken godoc
//
// @Summary Refresh SatuSehat Access Token
// @Description Memaksa request token baru dari API Satu Sehat Kemenkes (bypass cache)
// @Tags Satu Sehat - Auth
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /satusehat/reference/auth/token/refresh [post]
// @Security BearerAuth
func (h *AuthHandler) RefreshToken(c *gin.Context) {
data, err := h.service.RefreshToken(c.Request.Context())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil me-refresh token Satu Sehat", data)
}
@@ -0,0 +1,77 @@
package reference
import (
"net/http"
"service/internal/satusehat/reference/kfa"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type KFAHandler struct {
service kfa.Service
}
func NewKFAHandler(service kfa.Service) *KFAHandler {
return &KFAHandler{
service: service,
}
}
// GetByCode godoc
//
// @Summary Cari Produk KFA berdasarkan Kode
// @Description Mencari detail produk farmasi/alkes dari API Kamus Farmasi dan Alat Kesehatan (KFA) Satu Sehat
// @Tags Satu Sehat - KFA
// @Produce json
// @Param code path string true "Kode KFA (contoh: 93000469)"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/kfa/products/{code} [get]
// @Security BearerAuth
func (h *KFAHandler) GetByCode(c *gin.Context) {
code := c.Param("code")
data, err := h.service.GetByCode(c.Request.Context(), code)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data produk KFA", data)
}
// GetProducts godoc
//
// @Summary Daftar Produk KFA
// @Description Mengambil daftar produk KFA dengan kapabilitas paginasi
// @Tags Satu Sehat - KFA
// @Produce json
// @Param page query int false "Nomor Halaman (default: 1)"
// @Param size query int false "Jumlah Data (default: 10)"
// @Param product_type query string false "Tipe Produk (farmasi/alkes)"
// @Param keyword query string false "Kata Kunci Pencarian"
// @Param from_ query string false "Parameter waktu (from_)"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/kfa/products [get]
// @Security BearerAuth
func (h *KFAHandler) GetProducts(c *gin.Context) {
var params kfa.KFASearchParams
if err := c.ShouldBindQuery(&params); err != nil {
response.Error(c, http.StatusBadRequest, "Format parameter tidak valid", err.Error())
return
}
data, err := h.service.GetProducts(c.Request.Context(), params)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mengambil daftar produk KFA", data)
}
func (h *KFAHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/reference/kfa")
{
group.GET("/products/:code", h.GetByCode)
group.GET("/products", h.GetProducts)
}
}
@@ -0,0 +1,126 @@
package reference
import (
"net/http"
"service/internal/satusehat/reference/location"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type LocationHandler struct {
service location.Service
}
func NewLocationHandler(service location.Service) *LocationHandler {
return &LocationHandler{
service: service,
}
}
// GetByID godoc
//
// @Summary Cari Lokasi (Satu Sehat) berdasarkan ID
// @Description Mencari data Location FHIR Satu Sehat berdasarkan ID
// @Tags Satu Sehat - Location
// @Produce json
// @Param id path string true "Location ID"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/location/{id} [get]
// @Security BearerAuth
func (h *LocationHandler) GetByID(c *gin.Context) {
id := c.Param("id")
data, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data lokasi", data)
}
// Search godoc
//
// @Summary Pencarian Lokasi (Satu Sehat)
// @Description Mencari data Location berdasarkan parameter
// @Tags Satu Sehat - Location
// @Produce json
// @Param name query string false "Nama Lokasi"
// @Param organization query string false "ID Organisasi Pemilik"
// @Param identifier query string false "Identifier Lokasi"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/location [get]
// @Security BearerAuth
func (h *LocationHandler) Search(c *gin.Context) {
var params location.LocationSearchParams
if err := c.ShouldBindQuery(&params); err != nil {
response.Error(c, http.StatusBadRequest, "Format pencarian tidak valid", err.Error())
return
}
data, err := h.service.Search(c.Request.Context(), params)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data lokasi", data)
}
func (h *LocationHandler) Create(c *gin.Context) {
var payload map[string]interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
return
}
data, err := h.service.Create(c.Request.Context(), payload)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Berhasil membuat data lokasi", data)
}
func (h *LocationHandler) Update(c *gin.Context) {
id := c.Param("id")
var payload map[string]interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
return
}
data, err := h.service.Update(c.Request.Context(), id, payload)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mengubah data lokasi", data)
}
func (h *LocationHandler) Patch(c *gin.Context) {
id := c.Param("id")
var payload interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
return
}
data, err := h.service.Patch(c.Request.Context(), id, payload)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil melakukan patch data lokasi", data)
}
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
func (h *LocationHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/reference")
{
group.GET("/location/:id", h.GetByID)
group.GET("/location", h.Search)
group.POST("/location", h.Create)
group.PUT("/location/:id", h.Update)
group.PATCH("/location/:id", h.Patch)
}
}
@@ -0,0 +1,133 @@
package reference
import (
"net/http"
"service/internal/satusehat/reference/organization"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type OrganizationHandler struct {
service organization.Service
}
func NewOrganizationHandler(service organization.Service) *OrganizationHandler {
return &OrganizationHandler{
service: service,
}
}
// GetByID godoc
//
// @Summary Cari Organisasi (Satu Sehat) berdasarkan ID
// @Description Mencari data Organization FHIR Satu Sehat berdasarkan ID
// @Tags Satu Sehat - Organization
// @Produce json
// @Param id path string true "Organization ID"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/organization/{id} [get]
// @Security BearerAuth
func (h *OrganizationHandler) GetByID(c *gin.Context) {
id := c.Param("id")
data, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data organisasi", data)
}
// Search godoc
//
// @Summary Pencarian Organisasi (Satu Sehat)
// @Description Mencari data Organization berdasarkan Name atau PartOf
// @Tags Satu Sehat - Organization
// @Produce json
// @Param name query string false "Nama Organisasi"
// @Param partof query string false "ID Organisasi Induk"
// @Param identifier query string false "Identifier Organisasi"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/organization [get]
// @Security BearerAuth
func (h *OrganizationHandler) Search(c *gin.Context) {
var params organization.OrganizationSearchParams
if err := c.ShouldBindQuery(&params); err != nil {
response.Error(c, http.StatusBadRequest, "Format pencarian tidak valid", err.Error())
return
}
// Validasi mandiri sebelum menembak ke API Kemenkes
if params.Name == "" && params.PartOf == "" && params.Identifier == "" {
response.Error(c, http.StatusBadRequest, "Parameter pencarian tidak lengkap", "Harap masukkan minimal salah satu parameter query: name, partof, atau identifier")
return
}
data, err := h.service.Search(c.Request.Context(), params)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data organisasi", data)
}
func (h *OrganizationHandler) Create(c *gin.Context) {
var payload map[string]interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
return
}
data, err := h.service.Create(c.Request.Context(), payload)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Berhasil membuat data organisasi", data)
}
func (h *OrganizationHandler) Update(c *gin.Context) {
id := c.Param("id")
var payload map[string]interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
return
}
data, err := h.service.Update(c.Request.Context(), id, payload)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mengubah data organisasi", data)
}
func (h *OrganizationHandler) Patch(c *gin.Context) {
id := c.Param("id")
// JSON Patch biasanya array of objects, jadi gunakan interface{} general
var payload interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
return
}
data, err := h.service.Patch(c.Request.Context(), id, payload)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil melakukan patch data organisasi", data)
}
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
func (h *OrganizationHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/reference")
{
group.GET("/organization/:id", h.GetByID)
group.GET("/organization", h.Search)
group.POST("/organization", h.Create)
group.PUT("/organization/:id", h.Update)
group.PATCH("/organization/:id", h.Patch)
}
}
@@ -0,0 +1,127 @@
package reference
import (
"net/http"
"service/internal/satusehat/reference/patient"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
// PatientHandler menangani endpoint HTTP untuk resource Patient Satu Sehat.
type PatientHandler struct {
service patient.Service
}
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
func (h *PatientHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/reference")
{
group.GET("/patient/nik/:nik", h.GetByNIK)
group.GET("/patient/:id", h.GetByID)
group.GET("/patient", h.Search)
group.POST("/patient", h.Create)
}
}
// NewPatientHandler membuat instance baru dari PatientHandler.
func NewPatientHandler(service patient.Service) *PatientHandler {
return &PatientHandler{
service: service,
}
}
// GetByNIK godoc
//
// @Summary Cari Pasien (Satu Sehat) berdasarkan NIK
// @Description Mencari data pasien FHIR Satu Sehat berdasarkan Nomor Induk Kependudukan (NIK)
// @Tags Satu Sehat - Patient
// @Produce json
// @Param nik path string true "Nomor Induk Kependudukan"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/patient/nik/{nik} [get]
// @Security BearerAuth
func (h *PatientHandler) GetByNIK(c *gin.Context) {
nik := c.Param("nik")
data, err := h.service.GetByNIK(c.Request.Context(), nik)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data pasien", data)
}
// GetByID godoc
//
// @Summary Cari Pasien (Satu Sehat) berdasarkan ID
// @Description Mencari data pasien FHIR Satu Sehat berdasarkan IHS Number / ID
// @Tags Satu Sehat - Patient
// @Produce json
// @Param id path string true "IHS Number / Patient ID"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/patient/{id} [get]
// @Security BearerAuth
func (h *PatientHandler) GetByID(c *gin.Context) {
id := c.Param("id")
data, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data pasien", data)
}
// Search godoc
//
// @Summary Pencarian Pasien (Satu Sehat) Multi-Parameter
// @Description Mencari data pasien FHIR Satu Sehat berdasarkan kombinasi parameter (Nama, NIK, NIK Ibu, Tanggal Lahir, Gender)
// @Tags Satu Sehat - Patient
// @Produce json
// @Param nik query string false "Nomor Induk Kependudukan"
// @Param nik_ibu query string false "Nomor Induk Kependudukan Ibu (Untuk bayi)"
// @Param name query string false "Nama Pasien"
// @Param birthdate query string false "Tanggal Lahir (YYYY-MM-DD)"
// @Param gender query string false "Jenis Kelamin (male/female)"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/patient [get]
// @Security BearerAuth
func (h *PatientHandler) Search(c *gin.Context) {
var params patient.PatientSearchParams
if err := c.ShouldBindQuery(&params); err != nil {
response.Error(c, http.StatusBadRequest, "Format pencarian tidak valid", err.Error())
return
}
data, err := h.service.Search(c.Request.Context(), params)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data pasien", data)
}
// Create godoc
//
// @Summary Daftar Pasien Baru (Satu Sehat)
// @Description Mendaftarkan data pasien baru ke API Satu Sehat dan mengembalikan IHS Number
// @Tags Satu Sehat - Patient
// @Accept json
// @Produce json
// @Param request body patient.CreatePatientRequest true "Data Pasien Baru"
// @Success 201 {object} response.Response
// @Router /satusehat/reference/patient [post]
// @Security BearerAuth
func (h *PatientHandler) Create(c *gin.Context) {
var req patient.CreatePatientRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
return
}
data, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Berhasil mendaftarkan pasien", data)
}
@@ -0,0 +1,98 @@
package reference
import (
"net/http"
"service/internal/satusehat/reference/practitioner"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type PractitionerHandler struct {
service practitioner.Service
}
func NewPractitionerHandler(service practitioner.Service) *PractitionerHandler {
return &PractitionerHandler{
service: service,
}
}
// GetByNIK godoc
//
// @Summary Cari Tenaga Medis (Satu Sehat) berdasarkan NIK
// @Description Mencari data Practitioner FHIR Satu Sehat berdasarkan NIK
// @Tags Satu Sehat - Practitioner
// @Produce json
// @Param nik path string true "Nomor Induk Kependudukan"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/practitioner/nik/{nik} [get]
// @Security BearerAuth
func (h *PractitionerHandler) GetByNIK(c *gin.Context) {
nik := c.Param("nik")
data, err := h.service.GetByNIK(c.Request.Context(), nik)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data tenaga medis", data)
}
// GetByID godoc
//
// @Summary Cari Tenaga Medis (Satu Sehat) berdasarkan ID
// @Description Mencari data Practitioner FHIR Satu Sehat berdasarkan IHS Number / ID
// @Tags Satu Sehat - Practitioner
// @Produce json
// @Param id path string true "IHS Number / Practitioner ID"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/practitioner/{id} [get]
// @Security BearerAuth
func (h *PractitionerHandler) GetByID(c *gin.Context) {
id := c.Param("id")
data, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data tenaga medis", data)
}
// Search godoc
//
// @Summary Pencarian Tenaga Medis (Satu Sehat) Multi-Parameter
// @Description Mencari data Practitioner FHIR Satu Sehat berdasarkan parameter
// @Tags Satu Sehat - Practitioner
// @Produce json
// @Param nik query string false "Nomor Induk Kependudukan"
// @Param name query string false "Nama Tenaga Medis"
// @Param gender query string false "Jenis Kelamin (male/female)"
// @Param birthdate query string false "Tanggal Lahir (YYYY-MM-DD)"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/practitioner [get]
// @Security BearerAuth
func (h *PractitionerHandler) Search(c *gin.Context) {
var params practitioner.PractitionerSearchParams
if err := c.ShouldBindQuery(&params); err != nil {
response.Error(c, http.StatusBadRequest, "Format pencarian tidak valid", err.Error())
return
}
data, err := h.service.Search(c.Request.Context(), params)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mendapatkan data tenaga medis", data)
}
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
func (h *PractitionerHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/reference")
{
group.GET("/practitioner/nik/:nik", h.GetByNIK)
group.GET("/practitioner/:id", h.GetByID)
group.GET("/practitioner", h.Search)
}
}
@@ -0,0 +1,81 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/allergyintolerance"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type AllergyIntoleranceHandler struct{ service allergyintolerance.Service }
func NewAllergyIntoleranceHandler(s allergyintolerance.Service) *AllergyIntoleranceHandler {
return &AllergyIntoleranceHandler{service: s}
}
func (h *AllergyIntoleranceHandler) RegisterRoutes(router *gin.RouterGroup) {
g := router.Group("/satusehat/allergyintolerance")
g.POST("", h.Create)
g.GET("", h.Search)
g.GET("/:id", h.GetByID)
g.PUT("/:id", h.Update)
g.PATCH("/:id", h.Patch)
}
func (h *AllergyIntoleranceHandler) Create(c *gin.Context) {
var req allergyintolerance.AllergyIntoleranceRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
}
func (h *AllergyIntoleranceHandler) Update(c *gin.Context) {
var req allergyintolerance.AllergyIntoleranceRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *AllergyIntoleranceHandler) Patch(c *gin.Context) {
var req allergyintolerance.AllergyIntolerancePatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
return
}
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *AllergyIntoleranceHandler) GetByID(c *gin.Context) {
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *AllergyIntoleranceHandler) Search(c *gin.Context) {
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
@@ -0,0 +1,79 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/careplan"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type CarePlanHandler struct{ service careplan.Service }
func NewCarePlanHandler(s careplan.Service) *CarePlanHandler { return &CarePlanHandler{service: s} }
func (h *CarePlanHandler) RegisterRoutes(router *gin.RouterGroup) {
g := router.Group("/satusehat/careplan")
g.POST("", h.Create)
g.GET("", h.Search)
g.GET("/:id", h.GetByID)
g.PUT("/:id", h.Update)
g.PATCH("/:id", h.Patch)
}
func (h *CarePlanHandler) Create(c *gin.Context) {
var req careplan.CarePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
}
func (h *CarePlanHandler) Update(c *gin.Context) {
var req careplan.CarePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *CarePlanHandler) Patch(c *gin.Context) {
var req careplan.CarePlanPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
return
}
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *CarePlanHandler) GetByID(c *gin.Context) {
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *CarePlanHandler) Search(c *gin.Context) {
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
@@ -0,0 +1,81 @@
package usecase
import (
"net/http"
clinicalimpression "service/internal/satusehat/usecase/clinicalImpression"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type ClinicalImpressionHandler struct{ service clinicalimpression.Service }
func NewClinicalImpressionHandler(s clinicalimpression.Service) *ClinicalImpressionHandler {
return &ClinicalImpressionHandler{service: s}
}
func (h *ClinicalImpressionHandler) RegisterRoutes(router *gin.RouterGroup) {
g := router.Group("/satusehat/clinicalimpression")
g.POST("", h.Create)
g.GET("", h.Search)
g.GET("/:id", h.GetByID)
g.PUT("/:id", h.Update)
g.PATCH("/:id", h.Patch)
}
func (h *ClinicalImpressionHandler) Create(c *gin.Context) {
var req clinicalimpression.ClinicalImpressionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
}
func (h *ClinicalImpressionHandler) Update(c *gin.Context) {
var req clinicalimpression.ClinicalImpressionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *ClinicalImpressionHandler) Patch(c *gin.Context) {
var req clinicalimpression.ClinicalImpressionPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
return
}
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *ClinicalImpressionHandler) GetByID(c *gin.Context) {
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *ClinicalImpressionHandler) Search(c *gin.Context) {
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
@@ -0,0 +1,102 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/composition"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type CompositionHandler struct {
service composition.Service
}
func NewCompositionHandler(service composition.Service) *CompositionHandler {
return &CompositionHandler{
service: service,
}
}
func (h *CompositionHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/composition")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *CompositionHandler) Create(c *gin.Context) {
var req composition.CompositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created Composition", result.FullResponse)
}
func (h *CompositionHandler) Update(c *gin.Context) {
id := c.Param("id")
var req composition.CompositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated Composition", result.FullResponse)
}
func (h *CompositionHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req composition.CompositionPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched Composition", result.FullResponse)
}
func (h *CompositionHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Composition", result.FullResponse)
}
func (h *CompositionHandler) Search(c *gin.Context) {
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Compositions", result.FullResponse)
}
@@ -0,0 +1,102 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/condition"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type ConditionHandler struct {
service condition.Service
}
func NewConditionHandler(service condition.Service) *ConditionHandler {
return &ConditionHandler{
service: service,
}
}
func (h *ConditionHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/condition")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *ConditionHandler) Create(c *gin.Context) {
var req condition.ConditionRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created Condition", result)
}
func (h *ConditionHandler) Update(c *gin.Context) {
id := c.Param("id")
var req condition.ConditionRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated Condition", result)
}
func (h *ConditionHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req condition.ConditionPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched Condition", result)
}
func (h *ConditionHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Condition", result)
}
func (h *ConditionHandler) Search(c *gin.Context) {
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Conditions", result)
}
@@ -0,0 +1,81 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/diagnosticreport"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type DiagnosticReportHandler struct{ service diagnosticreport.Service }
func NewDiagnosticReportHandler(s diagnosticreport.Service) *DiagnosticReportHandler {
return &DiagnosticReportHandler{service: s}
}
func (h *DiagnosticReportHandler) RegisterRoutes(router *gin.RouterGroup) {
g := router.Group("/satusehat/diagnosticreport")
g.POST("", h.Create)
g.GET("", h.Search)
g.GET("/:id", h.GetByID)
g.PUT("/:id", h.Update)
g.PATCH("/:id", h.Patch)
}
func (h *DiagnosticReportHandler) Create(c *gin.Context) {
var req diagnosticreport.DiagnosticReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
}
func (h *DiagnosticReportHandler) Update(c *gin.Context) {
var req diagnosticreport.DiagnosticReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *DiagnosticReportHandler) Patch(c *gin.Context) {
var req diagnosticreport.DiagnosticReportPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
return
}
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *DiagnosticReportHandler) GetByID(c *gin.Context) {
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *DiagnosticReportHandler) Search(c *gin.Context) {
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
@@ -0,0 +1,149 @@
package usecase
import (
"errors"
"net/http"
"strconv"
"time"
"service/internal/interfaces/satusehat"
"service/internal/satusehat/usecase/encounter"
pkgErrors "service/pkg/errors"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type EncounterHandler struct {
service encounter.Service
}
func NewEncounterHandler(service encounter.Service) *EncounterHandler {
return &EncounterHandler{
service: service,
}
}
func (h *EncounterHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/encounter")
{
group.POST("", h.Create)
group.POST("/sync/:idxdaftar", h.SyncFromSIMRS)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
// handleSatuSehatError mengekstrak OperationOutcome agar JSON bisa tampil terstruktur
func handleSatuSehatError(c *gin.Context, err error) {
var ssErr *satusehat.ErrorOperationOutcome
if errors.As(err, &ssErr) {
c.Error(err) // Log error asli
c.JSON(ssErr.StatusCode, gin.H{
"status": "error",
"message": ssErr.Outcome,
"error": gin.H{
"severity": "error",
"timestamp": time.Now().UTC().Format(time.RFC3339),
},
})
return
}
appErr := pkgErrors.FromError(err)
response.ErrorWithLog(c, err, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
}
func (h *EncounterHandler) Create(c *gin.Context) {
var req encounter.EncounterRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
// Di sini, Anda bisa menyimpan result.RawResponse atau result.ID ke database log jika diperlukan.
// Contoh:
// go logService.Create(c.Request.Context(), "encounter_create", req, result.RawResponse, http.StatusCreated)
response.Success(c, http.StatusCreated, "Successfully created Encounter", result.FullResponse)
}
func (h *EncounterHandler) Update(c *gin.Context) {
id := c.Param("id")
var req encounter.EncounterRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated Encounter", result.FullResponse)
}
func (h *EncounterHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req encounter.EncounterPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched Encounter", result.FullResponse)
}
func (h *EncounterHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Encounter", result.FullResponse)
}
func (h *EncounterHandler) Search(c *gin.Context) {
// Get query parameters string natively and fetch it directly (like: ?patient=xxx&status=active)
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Encounters", result.FullResponse)
}
func (h *EncounterHandler) SyncFromSIMRS(c *gin.Context) {
idxdaftarStr := c.Param("idxdaftar")
idxdaftar, err := strconv.ParseInt(idxdaftarStr, 10, 64)
if err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format idxdaftar tidak valid", nil)
return
}
result, err := h.service.SyncFromSIMRS(c.Request.Context(), idxdaftar)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully synced Encounter from SIMRS", result.FullResponse)
}
@@ -0,0 +1,102 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/episodeofcare"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type EpisodeOfCareHandler struct {
service episodeofcare.Service
}
func NewEpisodeOfCareHandler(service episodeofcare.Service) *EpisodeOfCareHandler {
return &EpisodeOfCareHandler{
service: service,
}
}
func (h *EpisodeOfCareHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/episodeofcare")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *EpisodeOfCareHandler) Create(c *gin.Context) {
var req episodeofcare.EpisodeOfCareRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created EpisodeOfCare", result)
}
func (h *EpisodeOfCareHandler) Update(c *gin.Context) {
id := c.Param("id")
var req episodeofcare.EpisodeOfCareRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated EpisodeOfCare", result)
}
func (h *EpisodeOfCareHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req episodeofcare.EpisodeOfCarePatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched EpisodeOfCare", result)
}
func (h *EpisodeOfCareHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved EpisodeOfCare", result)
}
func (h *EpisodeOfCareHandler) Search(c *gin.Context) {
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved EpisodeOfCare records", result)
}
@@ -0,0 +1,102 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/imagingstudy"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type ImagingStudyHandler struct {
service imagingstudy.Service
}
func NewImagingStudyHandler(service imagingstudy.Service) *ImagingStudyHandler {
return &ImagingStudyHandler{
service: service,
}
}
func (h *ImagingStudyHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/imagingstudy")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *ImagingStudyHandler) Create(c *gin.Context) {
var req imagingstudy.ImagingStudyRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created ImagingStudy", result)
}
func (h *ImagingStudyHandler) Update(c *gin.Context) {
id := c.Param("id")
var req imagingstudy.ImagingStudyRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated ImagingStudy", result)
}
func (h *ImagingStudyHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req imagingstudy.ImagingStudyPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched ImagingStudy", result)
}
func (h *ImagingStudyHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved ImagingStudy", result)
}
func (h *ImagingStudyHandler) Search(c *gin.Context) {
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved ImagingStudies", result)
}
@@ -0,0 +1,81 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/immunization"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type ImmunizationHandler struct{ service immunization.Service }
func NewImmunizationHandler(s immunization.Service) *ImmunizationHandler {
return &ImmunizationHandler{service: s}
}
func (h *ImmunizationHandler) RegisterRoutes(router *gin.RouterGroup) {
g := router.Group("/satusehat/immunization")
g.POST("", h.Create)
g.GET("", h.Search)
g.GET("/:id", h.GetByID)
g.PUT("/:id", h.Update)
g.PATCH("/:id", h.Patch)
}
func (h *ImmunizationHandler) Create(c *gin.Context) {
var req immunization.ImmunizationRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
}
func (h *ImmunizationHandler) Update(c *gin.Context) {
var req immunization.ImmunizationRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *ImmunizationHandler) Patch(c *gin.Context) {
var req immunization.ImmunizationPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
return
}
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *ImmunizationHandler) GetByID(c *gin.Context) {
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *ImmunizationHandler) Search(c *gin.Context) {
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
@@ -0,0 +1,102 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/medication"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type MedicationHandler struct {
service medication.Service
}
func NewMedicationHandler(service medication.Service) *MedicationHandler {
return &MedicationHandler{
service: service,
}
}
func (h *MedicationHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/medication")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *MedicationHandler) Create(c *gin.Context) {
var req medication.MedicationRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created Medication", result.FullResponse)
}
func (h *MedicationHandler) Update(c *gin.Context) {
id := c.Param("id")
var req medication.MedicationRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated Medication", result.FullResponse)
}
func (h *MedicationHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req medication.MedicationPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched Medication", result.FullResponse)
}
func (h *MedicationHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Medication", result.FullResponse)
}
func (h *MedicationHandler) Search(c *gin.Context) {
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Medications", result.FullResponse)
}
@@ -0,0 +1,91 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/medicationdispense"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type MedicationDispenseHandler struct {
service medicationdispense.Service
}
func NewMedicationDispenseHandler(service medicationdispense.Service) *MedicationDispenseHandler {
return &MedicationDispenseHandler{service: service}
}
func (h *MedicationDispenseHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/medicationdispense")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *MedicationDispenseHandler) Create(c *gin.Context) {
var req medicationdispense.MedicationDispenseRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created MedicationDispense", result.FullResponse)
}
func (h *MedicationDispenseHandler) Update(c *gin.Context) {
id := c.Param("id")
var req medicationdispense.MedicationDispenseRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated MedicationDispense", result.FullResponse)
}
func (h *MedicationDispenseHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req medicationdispense.MedicationDispensePatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched MedicationDispense", result.FullResponse)
}
func (h *MedicationDispenseHandler) GetByID(c *gin.Context) {
result, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
}
func (h *MedicationDispenseHandler) Search(c *gin.Context) {
result, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
}
@@ -0,0 +1,91 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/medicationrequest"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type MedicationRequestHandler struct {
service medicationrequest.Service
}
func NewMedicationRequestHandler(service medicationrequest.Service) *MedicationRequestHandler {
return &MedicationRequestHandler{service: service}
}
func (h *MedicationRequestHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/medicationrequest")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *MedicationRequestHandler) Create(c *gin.Context) {
var req medicationrequest.MedicationRequestRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created MedicationRequest", result.FullResponse)
}
func (h *MedicationRequestHandler) Update(c *gin.Context) {
id := c.Param("id")
var req medicationrequest.MedicationRequestRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated MedicationRequest", result.FullResponse)
}
func (h *MedicationRequestHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req medicationrequest.MedicationRequestPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched MedicationRequest", result.FullResponse)
}
func (h *MedicationRequestHandler) GetByID(c *gin.Context) {
result, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
}
func (h *MedicationRequestHandler) Search(c *gin.Context) {
result, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
}
@@ -0,0 +1,91 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/medicationstatement"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type MedicationStatementHandler struct {
service medicationstatement.Service
}
func NewMedicationStatementHandler(service medicationstatement.Service) *MedicationStatementHandler {
return &MedicationStatementHandler{service: service}
}
func (h *MedicationStatementHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/medicationstatement")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *MedicationStatementHandler) Create(c *gin.Context) {
var req medicationstatement.MedicationStatementRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created MedicationStatement", result.FullResponse)
}
func (h *MedicationStatementHandler) Update(c *gin.Context) {
id := c.Param("id")
var req medicationstatement.MedicationStatementRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated MedicationStatement", result.FullResponse)
}
func (h *MedicationStatementHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req medicationstatement.MedicationStatementPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched MedicationStatement", result.FullResponse)
}
func (h *MedicationStatementHandler) GetByID(c *gin.Context) {
result, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
}
func (h *MedicationStatementHandler) Search(c *gin.Context) {
result, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
}
@@ -0,0 +1,102 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/observation"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type ObservationHandler struct {
service observation.Service
}
func NewObservationHandler(service observation.Service) *ObservationHandler {
return &ObservationHandler{
service: service,
}
}
func (h *ObservationHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/observation")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *ObservationHandler) Create(c *gin.Context) {
var req observation.ObservationRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created Observation", result.FullResponse)
}
func (h *ObservationHandler) Update(c *gin.Context) {
id := c.Param("id")
var req observation.ObservationRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated Observation", result.FullResponse)
}
func (h *ObservationHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req observation.ObservationPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched Observation", result.FullResponse)
}
func (h *ObservationHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Observation", result.FullResponse)
}
func (h *ObservationHandler) Search(c *gin.Context) {
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Observations", result.FullResponse)
}
@@ -0,0 +1,102 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/procedure"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type ProcedureHandler struct {
service procedure.Service
}
func NewProcedureHandler(service procedure.Service) *ProcedureHandler {
return &ProcedureHandler{
service: service,
}
}
func (h *ProcedureHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/procedure")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *ProcedureHandler) Create(c *gin.Context) {
var req procedure.ProcedureRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created Procedure", result.FullResponse)
}
func (h *ProcedureHandler) Update(c *gin.Context) {
id := c.Param("id")
var req procedure.ProcedureRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated Procedure", result.FullResponse)
}
func (h *ProcedureHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req procedure.ProcedurePatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched Procedure", result.FullResponse)
}
func (h *ProcedureHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Procedure", result.FullResponse)
}
func (h *ProcedureHandler) Search(c *gin.Context) {
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved Procedures", result.FullResponse)
}
@@ -0,0 +1,91 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/questionnaireresponse"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type QuestionnaireResponseHandler struct {
service questionnaireresponse.Service
}
func NewQuestionnaireResponseHandler(service questionnaireresponse.Service) *QuestionnaireResponseHandler {
return &QuestionnaireResponseHandler{service: service}
}
func (h *QuestionnaireResponseHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/questionnaireresponse")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *QuestionnaireResponseHandler) Create(c *gin.Context) {
var req questionnaireresponse.QuestionnaireResponseRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created QuestionnaireResponse", result.FullResponse)
}
func (h *QuestionnaireResponseHandler) Update(c *gin.Context) {
id := c.Param("id")
var req questionnaireresponse.QuestionnaireResponseRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated QuestionnaireResponse", result.FullResponse)
}
func (h *QuestionnaireResponseHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req questionnaireresponse.QuestionnaireResponsePatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched QuestionnaireResponse", result.FullResponse)
}
func (h *QuestionnaireResponseHandler) GetByID(c *gin.Context) {
result, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
}
func (h *QuestionnaireResponseHandler) Search(c *gin.Context) {
result, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
}
@@ -0,0 +1,102 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/servicerequest"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type ServiceRequestHandler struct {
service servicerequest.Service
}
func NewServiceRequestHandler(service servicerequest.Service) *ServiceRequestHandler {
return &ServiceRequestHandler{
service: service,
}
}
func (h *ServiceRequestHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/servicerequest")
{
group.POST("", h.Create)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
group.PATCH("/:id", h.Patch)
}
}
func (h *ServiceRequestHandler) Create(c *gin.Context) {
var req servicerequest.ServiceRequestRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Successfully created ServiceRequest", result.FullResponse)
}
func (h *ServiceRequestHandler) Update(c *gin.Context) {
id := c.Param("id")
var req servicerequest.ServiceRequestRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
return
}
result, err := h.service.Update(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully updated ServiceRequest", result.FullResponse)
}
func (h *ServiceRequestHandler) Patch(c *gin.Context) {
id := c.Param("id")
var req servicerequest.ServiceRequestPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
customErr := validator.TranslateError(err)
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
return
}
result, err := h.service.Patch(c.Request.Context(), id, req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully patched ServiceRequest", result.FullResponse)
}
func (h *ServiceRequestHandler) GetByID(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved ServiceRequest", result.FullResponse)
}
func (h *ServiceRequestHandler) Search(c *gin.Context) {
queryParams := c.Request.URL.Query()
result, err := h.service.Search(c.Request.Context(), queryParams)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully retrieved ServiceRequests", result.FullResponse)
}
@@ -0,0 +1,79 @@
package usecase
import (
"net/http"
"service/internal/satusehat/usecase/specimen"
"service/pkg/response"
"service/pkg/utils/validator"
"github.com/gin-gonic/gin"
)
type SpecimenHandler struct{ service specimen.Service }
func NewSpecimenHandler(s specimen.Service) *SpecimenHandler { return &SpecimenHandler{service: s} }
func (h *SpecimenHandler) RegisterRoutes(router *gin.RouterGroup) {
g := router.Group("/satusehat/specimen")
g.POST("", h.Create)
g.GET("", h.Search)
g.GET("/:id", h.GetByID)
g.PUT("/:id", h.Update)
g.PATCH("/:id", h.Patch)
}
func (h *SpecimenHandler) Create(c *gin.Context) {
var req specimen.SpecimenRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
}
func (h *SpecimenHandler) Update(c *gin.Context) {
var req specimen.SpecimenRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
return
}
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *SpecimenHandler) Patch(c *gin.Context) {
var req specimen.SpecimenPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
return
}
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *SpecimenHandler) GetByID(c *gin.Context) {
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
func (h *SpecimenHandler) Search(c *gin.Context) {
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Success", res.FullResponse)
}
@@ -0,0 +1,232 @@
package middleware
import (
"context"
"log"
"net/http"
"os"
"strings"
"time"
"service/pkg/logger" // Tambahkan import ini
"github.com/google/uuid"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func CORSMiddleware() gin.HandlerFunc {
// Development mode - izinkan semua origin (hanya untuk dev!)
if os.Getenv("APP_ENV") == "development" && os.Getenv("CORS_ALLOW_ALL") == "true" {
log.Println("WARNING: CORS allowing all origins (development mode)")
// Gunakan config khusus untuk allow all origins
config := cors.DefaultConfig()
config.AllowAllOrigins = true
config.AllowCredentials = false // Tidak bisa digunakan dengan AllowAllOrigins
config.AllowMethods = []string{
"GET", "POST", "PUT", "PATCH", "DELETE",
"HEAD", "OPTIONS",
}
config.AllowHeaders = []string{
"Origin",
"Content-Length",
"Content-Type",
"Authorization",
"X-Requested-With",
"X-API-Key",
"X-CSRF-Token",
"X-Custom-Header",
"Accept",
"Accept-Language",
"Accept-Encoding",
"Access-Control-Request-Headers",
"Access-Control-Request-Method",
// Headers tambahan untuk Nuxt 3
"x-use-fetch",
"x-nuxt-base-url",
"x-forwarded-for",
"x-forwarded-proto",
"x-forwarded-host",
}
config.MaxAge = 12 * time.Hour
return cors.New(config)
}
// Config untuk specific origins
config := cors.DefaultConfig()
// Baca allowed origins dari environment variable
// Format: CORS_ORIGINS=http://localhost:3000,http://localhost:3001,https://myapp.com
originsEnv := os.Getenv("CORS_ORIGINS")
if originsEnv != "" {
// Split by comma dan trim spaces
origins := strings.Split(originsEnv, ",")
for i, origin := range origins {
origins[i] = strings.TrimSpace(origin)
}
config.AllowOrigins = origins
log.Printf("CORS: Using origins from environment: %v", config.AllowOrigins)
} else {
// Default origins untuk Nuxt 3 development
config.AllowOrigins = []string{
"http://localhost:3000", // Nuxt 3 default
"http://localhost:3001", // Nuxt 3 alternatif
"http://localhost:3002", // Nuxt 3 alternatif
"http://localhost:3005", // Nuxt 3 port Anda
"http://localhost:8080", // Common dev port
"http://localhost:5173", // Vite default port
"http://localhost:5174", // Vite alternatif
"https://localhost:3000", // HTTPS Nuxt
"https://localhost:8080", // HTTPS common
"http://meninjar.dev.rssa.id:8094", // Domain production Anda
}
log.Printf("CORS: Using default origins: %v", config.AllowOrigins)
}
// Method yang diizinkan untuk Nuxt 3 + TypeScript
config.AllowMethods = []string{
"GET", "POST", "PUT", "PATCH", "DELETE",
"HEAD", "OPTIONS",
}
// Headers yang diizinkan untuk Nuxt 3 + Axios
config.AllowHeaders = []string{
"Origin",
"Content-Length",
"Content-Type",
"Authorization",
"X-Requested-With",
"X-API-Key",
"X-CSRF-Token",
"X-Custom-Header",
"Accept",
"Accept-Language",
"Accept-Encoding",
"Access-Control-Request-Headers",
"Access-Control-Request-Method",
// Headers tambahan untuk Nuxt 3
"x-use-fetch",
"x-nuxt-base-url",
"x-forwarded-for",
"x-forwarded-proto",
"x-forwarded-host",
}
// Izinkan credentials (penting untuk Nuxt 3)
config.AllowCredentials = true
// Preflight cache duration
config.MaxAge = 12 * time.Hour
return cors.New(config)
}
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
// Ambil atau Generate Request ID untuk Tracing (Correlation ID)
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
// Injeksi request_id ke dalam context request
ctx := context.WithValue(c.Request.Context(), "request_id", requestID)
c.Request = c.Request.WithContext(ctx)
c.Header("X-Request-ID", requestID)
// Process request
c.Next()
// Log using custom logger
latency := time.Since(start)
clientIP := c.ClientIP()
method := c.Request.Method
statusCode := c.Writer.Status()
fields := []logger.Field{
logger.String("ip", clientIP),
logger.String("method", method),
logger.String("path", path),
logger.Int("status", statusCode),
logger.Duration("latency", latency),
logger.String("user_agent", c.Request.UserAgent()),
}
if raw != "" {
fields = append(fields, logger.String("query", raw))
}
if len(c.Errors) > 0 {
fields = append(fields, logger.String("error", c.Errors.String()))
}
logCtx := logger.Default().WithContext(ctx)
// Use appropriate log level based on status code
if statusCode >= 500 {
logCtx.Error("HTTP Request", fields...)
} else if statusCode >= 400 {
logCtx.Warn("HTTP Request", fields...)
} else {
logCtx.Info("HTTP Request", fields...)
}
}
}
func ErrorMiddleware() gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
logger.Default().WithContext(c.Request.Context()).Error("Panic recovered", logger.Any("panic", recovered))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error",
"code": "INTERNAL_ERROR",
})
})
}
func SecurityMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 1. HSTS (Strict-Transport-Security)
// Memaksa browser hanya menggunakan HTTPS selama 1 tahun, termasuk subdomain.
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
// 2. Content Security Policy (CSP) - Code Injection Protection
// Kita longgarkan khusus untuk path /swagger agar UI bisa melakukan load JavaScript & CSS inline bawaannya.
if strings.HasPrefix(c.Request.URL.Path, "/swagger") {
c.Header("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'")
} else {
c.Header("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content")
}
// 3. X-Content-Type-Options
// Mencegah browser menebak (sniffing) MIME type.
c.Header("X-Content-Type-Options", "nosniff")
// 4. X-Frame-Options (Clickjacking Protection)
// Mencegah website di-embed dalam iframe orang lain.
c.Header("X-Frame-Options", "DENY")
// 5. X-XSS-Protection
// Layer pertahanan lama untuk browser lama (Legacy), tapi tetap bagus untuk ada.
c.Header("X-XSS-Protection", "1; mode=block")
// 6. Referrer-Policy
// Menjaga privasi user saat klik link keluar dari aplikasi Anda.
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
// 7. Permissions-Policy (Feature Policy)
// Mematikan fitur browser yang tidak dipakai (kamera, mic, lokasi) untuk mengurangi attack vector.
c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=()")
// Remove Information Leakage
c.Header("Server", "Unknown") // Atau hapus total
c.Header("X-Powered-By", "")
c.Next()
}
}
@@ -0,0 +1,574 @@
package middleware
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/big"
"net/http"
"os"
pkgErrors "service/pkg/errors"
"strings"
"sync"
"time"
"service/internal/infrastructure/cache"
"service/internal/infrastructure/config"
"service/pkg/logger"
"service/pkg/response"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/sync/singleflight"
)
// Definisi Error kustom untuk autentikasi
var (
ErrInvalidToken = errors.New("invalid token")
ErrMissingClaims = errors.New("missing claims")
ErrTokenExpired = errors.New("token expired")
ErrInvalidSignature = errors.New("invalid signature")
ErrInvalidIssuer = errors.New("invalid issuer")
ErrInvalidAudience = errors.New("invalid audience")
ErrMissingAuthHeader = errors.New("missing authorization header")
ErrInvalidAuthHeader = errors.New("invalid authorization header format")
)
// JWTClaims menyimpan struktur payload token yang terekstrak
type JWTClaims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
Name string `json:"name"`
}
// AuthProvider interface for different authentication methods
type AuthProvider interface {
ValidateToken(tokenString string) (*JWTClaims, error)
Name() string
}
// ProviderFactory creates authentication providers based on configuration
type ProviderFactory struct {
config *config.Config
}
func NewProviderFactory(config *config.Config) *ProviderFactory {
return &ProviderFactory{
config: config,
}
}
func (f *ProviderFactory) CreateProviders() []AuthProvider {
var providers []AuthProvider
reqLogger := logger.Default()
reqLogger.Info("Creating authentication providers",
logger.String("auth_type", f.config.Auth.Type),
logger.Bool("keycloak_enabled", f.config.Keycloak.Enabled),
logger.String("keycloak_issuer", f.config.Keycloak.Issuer),
logger.Int("static_tokens_len", len(f.config.Auth.StaticTokens)),
logger.String("fallback_to", f.config.Auth.FallbackTo),
)
switch f.config.Auth.Type {
case "static":
if len(f.config.Auth.StaticTokens) > 0 {
providers = append(providers, NewStaticTokenProvider(f.config.Auth.StaticTokens))
} else {
reqLogger.Warn("No static tokens configured for static auth type", logger.String("type", "static"))
}
case "jwt":
providers = append(providers, NewJWTAuthProvider())
reqLogger.Info("JWT provider added")
case "keycloak":
if f.config.Keycloak.Issuer != "" {
providers = append(providers, NewKeycloakAuthProvider(f.config))
reqLogger.Info("Keycloak provider added")
} else {
reqLogger.Warn("Keycloak issuer not configured for keycloak auth type", logger.String("type", "keycloak"))
}
case "hybrid":
if f.config.Keycloak.Issuer != "" {
providers = append(providers, NewKeycloakAuthProvider(f.config))
reqLogger.Info("Keycloak provider added for hybrid")
} else {
reqLogger.Warn("Keycloak issuer not configured for hybrid auth type", logger.String("type", "keycloak"))
}
switch f.config.Auth.FallbackTo {
case "static":
if len(f.config.Auth.StaticTokens) > 0 {
providers = append(providers, NewStaticTokenProvider(f.config.Auth.StaticTokens))
} else {
reqLogger.Warn("No static tokens configured for hybrid fallback", logger.String("type", "static"))
}
case "jwt":
providers = append(providers, NewJWTAuthProvider())
default:
providers = append(providers, NewJWTAuthProvider())
reqLogger.Info("JWT fallback provider added as default")
}
default:
providers = append(providers, NewJWTAuthProvider())
}
return providers
}
// StaticTokenProvider handles static token authentication
type StaticTokenProvider struct {
tokens map[string]bool
}
func NewStaticTokenProvider(tokens []string) *StaticTokenProvider {
tokenMap := make(map[string]bool)
for _, token := range tokens {
if token != "" {
tokenMap[token] = true
}
}
return &StaticTokenProvider{tokens: tokenMap}
}
func (s *StaticTokenProvider) ValidateToken(tokenString string) (*JWTClaims, error) {
if !s.tokens[tokenString] {
return nil, ErrInvalidToken
}
return &JWTClaims{
UserID: "static-user",
Username: "static-user",
Email: "[email protected]",
Role: "user",
Name: "Static User",
}, nil
}
func (s *StaticTokenProvider) Name() string {
return "static"
}
// JWTAuthProvider handles JWT authentication
type JWTAuthProvider struct {
secret string
}
func NewJWTAuthProvider() *JWTAuthProvider {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
secret = "fallback_secret_key_change_in_production"
}
return &JWTAuthProvider{secret: secret}
}
func (j *JWTAuthProvider) ValidateToken(tokenString string) (*JWTClaims, error) {
parsedToken, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(j.secret), nil
})
if err != nil || !parsedToken.Valid {
return nil, ErrInvalidToken
}
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrMissingClaims
}
return &JWTClaims{
UserID: fmt.Sprintf("%v", claims["user_id"]),
Email: fmt.Sprintf("%v", claims["email"]),
Role: fmt.Sprintf("%v", claims["role_id"]),
Name: fmt.Sprintf("%v", claims["name"]),
}, nil
}
func (j *JWTAuthProvider) Name() string {
return "jwt"
}
// KeycloakAuthProvider handles Keycloak JWT authentication
type KeycloakAuthProvider struct {
jwksCache *JwksCache
config *config.Config
}
func NewKeycloakAuthProvider(cfg *config.Config) *KeycloakAuthProvider {
return &KeycloakAuthProvider{
jwksCache: NewJwksCache(cfg),
config: cfg,
}
}
func (k *KeycloakAuthProvider) ValidateToken(tokenString string) (*JWTClaims, error) {
parsedToken, _, err := jwt.NewParser().ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, ErrInvalidToken
}
// Extract claims for logging
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrMissingClaims
}
// Check if token is expired
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return nil, ErrTokenExpired
}
}
// Pastikan token yang diterima adalah Access Token ("Bearer"), bukan ID Token
if typ, ok := claims["typ"].(string); ok {
if typ != "Bearer" {
return nil, fmt.Errorf("invalid token type: expected Bearer, got %s", typ)
}
}
// Now parse with verification
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verify signing method
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
// Dihilangkan logger Warn agar tidak menyebabkan log spam ketika mekanisme fallback JWT aktif
return nil, ErrInvalidSignature
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
key, err := k.jwksCache.GetKey(kid)
if err != nil {
return nil, err
}
return key, nil
}, jwt.WithIssuer(k.config.Keycloak.Issuer))
if err != nil {
// Return specific error based on the error type
if strings.Contains(err.Error(), "expired") {
return nil, ErrTokenExpired
} else if strings.Contains(err.Error(), "signature") {
return nil, ErrInvalidSignature
} else if strings.Contains(err.Error(), "issuer") {
return nil, ErrInvalidIssuer
}
return nil, fmt.Errorf("invalid token: %v", err)
}
if !token.Valid {
return nil, ErrInvalidToken
}
// Extract claims
claims, ok = token.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrMissingClaims
}
// Validasi custom untuk Audience (aud) atau Authorized Party (azp)
// Keycloak menempatkan Client ID di 'azp' untuk Access Token
expectedAudience := k.config.Keycloak.Audience
if expectedAudience != "" {
validAudience := false
// 1. Cek klaim azp (Authorized Party)
if azp := getClaimString(claims, "azp"); azp == expectedAudience {
validAudience = true
}
// 2. Cek klaim aud (Audience) jika azp tidak cocok
if !validAudience {
if audValue, ok := claims["aud"]; ok {
if audList, err := extractAudience(audValue); err == nil {
for _, a := range audList {
if a == expectedAudience {
validAudience = true
break
}
}
}
}
}
if !validAudience {
return nil, ErrInvalidAudience
}
}
// Validate required claims
userID := getClaimString(claims, "sub")
if userID == "" {
return nil, ErrMissingClaims
}
// Ekstraksi nested roles dari Keycloak (realm_access.roles)
var roleStr string
if realmAccess, ok := claims["realm_access"].(map[string]interface{}); ok {
if roles, ok := realmAccess["roles"].([]interface{}); ok {
var roleList []string
for _, r := range roles {
roleList = append(roleList, fmt.Sprintf("%v", r))
}
roleStr = strings.Join(roleList, ",") // Menggabungkan array role menjadi string: "admin,user"
}
}
return &JWTClaims{
UserID: userID,
Username: getClaimString(claims, "preferred_username"),
Email: getClaimString(claims, "email"),
Role: roleStr,
Name: getClaimString(claims, "name"),
}, nil
}
func (k *KeycloakAuthProvider) Name() string {
return "keycloak"
}
// AuthMiddleware provides flexible authentication based on configuration and implements redis blacklist check
func AuthMiddleware(cfg *config.Config, cacheManager *cache.Manager) gin.HandlerFunc {
factory := NewProviderFactory(cfg)
providers := factory.CreateProviders()
// Validate that we have at least one provider
if len(providers) == 0 {
return func(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "authentication service not configured"})
}
}
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": ErrMissingAuthHeader.Error()})
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": ErrInvalidAuthHeader.Error()})
return
}
tokenString := parts[1]
// Cek Blacklist Token (User yang sudah logout dilarang menggunakan token yang sama)
if cacheManager != nil {
var isBlacklisted bool
if err := cacheManager.Get(c.Request.Context(), "blacklist_token:"+tokenString, &isBlacklisted); err == nil && isBlacklisted {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token has been revoked or logged out"})
return
}
}
// Coba setiap provider sampai salah satu berhasil
var claims *JWTClaims
var err error
var providerName string
providerErrorDetails := make(map[string]string)
for _, provider := range providers {
claims, err = provider.ValidateToken(tokenString)
if err == nil {
providerName = provider.Name()
break // Berhenti jika ada yang berhasil
}
providerErrorDetails[provider.Name()] = err.Error()
}
if err != nil {
var finalErr error
if errors.Is(err, ErrTokenExpired) {
finalErr = pkgErrors.UnauthorizedError().
Code(pkgErrors.ErrCodeTokenExpired).
Message(pkgErrors.GetLocalizedMessage(pkgErrors.ErrCodeTokenExpired, "id", "Token telah kadaluarsa")).
Metadata("provider_errors", providerErrorDetails).Build()
} else {
finalErr = pkgErrors.UnauthorizedError().
Code(pkgErrors.ErrCodeInvalidToken).
Message(pkgErrors.GetLocalizedMessage(pkgErrors.ErrCodeInvalidToken, "id", "Token tidak valid")).
Metadata("provider_errors", providerErrorDetails).Build()
}
appErr := pkgErrors.FromError(finalErr)
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
c.Abort()
return
}
// Set informasi pengguna di konteks
if claims != nil {
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("email", claims.Email)
c.Set("role", claims.Role)
c.Set("name", claims.Name)
c.Set("role_id", claims.Role) // Kompatibilitas untuk handler lama
c.Set("token", tokenString) // Kompatibilitas untuk handler lama
c.Set("auth_provider", providerName)
}
c.Next()
}
}
// InitializeAuth initializes authentication configuration
func InitializeAuth(cfg *config.Config) {
// This function can be used to initialize global auth settings if needed
logger.Default().Info("Authentication initialized", logger.String("auth_type", cfg.Auth.Type))
}
// Helper functions
func getClaimString(claims jwt.MapClaims, key string) string {
if value, ok := claims[key]; ok && value != nil {
if str, ok := value.(string); ok {
return str
}
}
return ""
}
// extractAudience parses audience claim which can be a string or an array of strings
func extractAudience(audValue interface{}) ([]string, error) {
switch v := audValue.(type) {
case string:
return []string{v}, nil
case []interface{}:
var auds []string
for _, a := range v {
if s, ok := a.(string); ok {
auds = append(auds, s)
}
}
return auds, nil
default:
return nil, errors.New("invalid audience format")
}
}
// JwksCache and related functions
type JwksCache struct {
mu sync.RWMutex
keys map[string]*rsa.PublicKey
expiresAt time.Time
sfGroup singleflight.Group
config *config.Config
}
func NewJwksCache(cfg *config.Config) *JwksCache {
return &JwksCache{
keys: make(map[string]*rsa.PublicKey),
config: cfg,
}
}
func (c *JwksCache) GetKey(kid string) (*rsa.PublicKey, error) {
c.mu.RLock()
if key, ok := c.keys[kid]; ok && time.Now().Before(c.expiresAt) {
c.mu.RUnlock()
return key, nil
}
c.mu.RUnlock()
// Fetch keys with singleflight to avoid concurrent fetches
v, err, _ := c.sfGroup.Do("fetch_jwks", func() (interface{}, error) {
return c.fetchKeys()
})
if err != nil {
return nil, err
}
keys := v.(map[string]*rsa.PublicKey)
c.mu.Lock()
c.keys = keys
c.expiresAt = time.Now().Add(1 * time.Hour) // cache for 1 hour
c.mu.Unlock()
key, ok := keys[kid]
if !ok {
return nil, fmt.Errorf("key with kid %s not found", kid)
}
return key, nil
}
func (c *JwksCache) fetchKeys() (map[string]*rsa.PublicKey, error) {
if c.config.Keycloak.Issuer == "" {
return nil, fmt.Errorf("keycloak issuer is not configured")
}
jwksURL := c.config.Keycloak.JwksURL
if jwksURL == "" {
// Construct JWKS URL from issuer if not explicitly provided
jwksURL = c.config.Keycloak.Issuer + "/protocol/openid-connect/certs"
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(jwksURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch JWKS: HTTP %d", resp.StatusCode)
}
var jwksData struct {
Keys []struct {
Kid string `json:"kid"`
Kty string `json:"kty"`
N string `json:"n"`
E string `json:"e"`
} `json:"keys"`
}
if err := json.NewDecoder(resp.Body).Decode(&jwksData); err != nil {
return nil, err
}
keys := make(map[string]*rsa.PublicKey)
for _, key := range jwksData.Keys {
if key.Kty != "RSA" {
continue
}
pubKey, err := parseRSAPublicKey(key.N, key.E)
if err != nil {
continue
}
keys[key.Kid] = pubKey
}
return keys, nil
}
// parseRSAPublicKey parses RSA public key components from base64url strings
func parseRSAPublicKey(nStr, eStr string) (*rsa.PublicKey, error) {
nBytes, err := base64.RawURLEncoding.DecodeString(nStr)
if err != nil {
return nil, err
}
eBytes, err := base64.RawURLEncoding.DecodeString(eStr)
if err != nil {
return nil, err
}
n := new(big.Int).SetBytes(nBytes)
e := int(new(big.Int).SetBytes(eBytes).Int64())
return &rsa.PublicKey{
N: n,
E: e,
}, nil
}
@@ -0,0 +1,183 @@
// internal/infrastructure/transport/http/middleware/rate_limit.go
package middleware
import (
"context"
"net/http"
"sync"
"time"
"service/internal/infrastructure/cache"
"service/pkg/logger" // Pastikan import ini benar
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
// RateLimitMiddleware creates rate limiting middleware using Redis cache
func RateLimitMiddleware(cacheManager *cache.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
clientIP := c.ClientIP()
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
defer cancel()
// Increment rate limit counter
count, err := cacheManager.IncrementRateLimit(ctx, clientIP)
if err != nil {
// PERBAIKAN: Gunakan logger baru dengan konteks dan field terstruktur
logger.Default().WithContext(ctx).
Error("Failed to increment rate limit for IP",
logger.ErrorField(err),
logger.String("client_ip", clientIP),
)
// Allow request to proceed if cache is unavailable
c.Next()
return
}
// Set TTL on first request
if count == 1 {
if err := cacheManager.SetRateLimit(ctx, clientIP, count); err != nil {
// PERBAIKAN: Gunakan logger baru
logger.Default().WithContext(ctx).
Error("Failed to set rate limit TTL for IP",
logger.ErrorField(err),
logger.String("client_ip", clientIP),
)
}
}
// Check if rate limit exceeded (60 requests per minute)
if count > 60 {
// PERBAIKAN: Tambahkan log saat rate limit terlampaui
logger.Default().WithContext(ctx).
Warn("Rate limit exceeded for IP",
logger.String("client_ip", clientIP),
logger.Int64("count", count),
)
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Too many requests",
"code": "RATE_LIMIT_EXCEEDED",
"retry_after": 60,
})
c.Abort()
return
}
c.Next()
}
}
// RateLimitByToken creates rate limiting middleware based on auth token
func RateLimitByToken(cacheManager *cache.Manager, requestsPerMinute int) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.Next()
return
}
// Extract token from Bearer format
if len(token) > 7 && token[:7] == "Bearer " {
token = token[7:]
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
defer cancel()
// Increment rate limit counter
count, err := cacheManager.IncrementRateLimit(ctx, "token:"+token)
if err != nil {
// PERBAIKAN: Gunakan logger baru
logger.Default().WithContext(ctx).
Error("Failed to increment token rate limit",
logger.ErrorField(err),
logger.String("token_prefix", token[:minLen(len(token), 10)]+"..."), // Jangan log token utuh
)
c.Next()
return
}
// Set TTL on first request
if count == 1 {
if err := cacheManager.SetRateLimit(ctx, "token:"+token, count); err != nil {
// PERBAIKAN: Gunakan logger baru
logger.Default().WithContext(ctx).
Error("Failed to set token rate limit TTL",
logger.ErrorField(err),
logger.String("token_prefix", token[:minLen(len(token), 10)]+"..."),
)
}
}
// Check if rate limit exceeded
if count > int64(requestsPerMinute) {
// PERBAIKAN: Tambahkan log saat rate limit token terlampaui
logger.Default().WithContext(ctx).
Warn("Rate limit exceeded for token",
logger.String("token_prefix", token[:minLen(len(token), 10)]+"..."),
logger.Int64("count", count),
)
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Too many requests for this token",
"code": "TOKEN_RATE_LIMIT_EXCEEDED",
"retry_after": 60,
})
c.Abort()
return
}
c.Next()
}
}
// Helper function untuk menghindari error jika token lebih pendek dari 10 karakter
func minLen(a, b int) int {
if a < b {
return a
}
return b
}
// Variabel global untuk menyimpan limiter per-client IP/Identifier untuk memory rate limiter
var (
visitors = make(map[string]*rate.Limiter)
mu sync.Mutex
)
// getVisitor mengambil atau membuat limiter baru untuk client identifier tertentu
func getVisitor(identifier string, r rate.Limit, b int) *rate.Limiter {
mu.Lock()
defer mu.Unlock()
limiter, exists := visitors[identifier]
if !exists {
limiter = rate.NewLimiter(r, b)
visitors[identifier] = limiter
}
return limiter
}
// MemoryRateLimitMiddleware membatasi jumlah request per client secara lokal di memori.
// requestsPerSecond: jumlah hit yang diizinkan per detik.
// burstSize: jumlah hit maksimal dalam satu waktu (burst).
func MemoryRateLimitMiddleware(requestsPerSecond float64, burstSize int) gin.HandlerFunc {
limit := rate.Limit(requestsPerSecond)
return func(c *gin.Context) {
clientIdentifier := c.ClientIP()
limiter := getVisitor(clientIdentifier, limit, burstSize)
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"status": "error",
"message": "Terlalu banyak permintaan ke API Satu Sehat. Silakan coba beberapa saat lagi.",
"code": "RATE_LIMIT_EXCEEDED",
})
c.Abort()
return
}
c.Next()
}
}
@@ -0,0 +1,263 @@
package routes
import (
"net/http"
"time"
"service/internal/infrastructure/cache"
"service/internal/infrastructure/config"
"service/internal/infrastructure/transport/http/middleware"
antrolHttp "service/internal/infrastructure/transport/http/handlers/bpjs/antrol"
aplicareHttp "service/internal/infrastructure/transport/http/handlers/bpjs/aplicare"
apotekHttp "service/internal/infrastructure/transport/http/handlers/bpjs/apotek"
vclaimHttp "service/internal/infrastructure/transport/http/handlers/bpjs/vclaim"
authHttp "service/internal/infrastructure/transport/http/handlers/main/auth"
healthHttp "service/internal/infrastructure/transport/http/handlers/main/health"
roleHttp "service/internal/infrastructure/transport/http/handlers/main/master/roles"
satuSehatRefHttp "service/internal/infrastructure/transport/http/handlers/satusehat/reference"
satuSehatUsecaseHttp "service/internal/infrastructure/transport/http/handlers/satusehat/usecase"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// ModuleHandlers menampung seluruh HTTP handler opsional dari berbagai modul.
// Jika sebuah handler bernilai nil, maka rutenya akan otomatis diabaikan (disabled).
type ModuleHandlers struct {
Auth *authHttp.AuthHandler
RolePages *roleHttp.RolPagesHandler
RolePermission *roleHttp.RolPermissionHandler
RoleMaster *roleHttp.RoleMasterHandler
Sep *vclaimHttp.SepHandler
Peserta *vclaimHttp.PesertaHandler
Antrol *antrolHttp.AntrolHandler
AplicareBed *aplicareHttp.BedHandler
ApotekReference *apotekHttp.ReferenceHandler
SatuSehatAuth *satuSehatRefHttp.AuthHandler
SatuSehatPatient *satuSehatRefHttp.PatientHandler
SatuSehatPractitioner *satuSehatRefHttp.PractitionerHandler
SatuSehatOrganization *satuSehatRefHttp.OrganizationHandler
SatuSehatLocation *satuSehatRefHttp.LocationHandler
SatuSehatKFA *satuSehatRefHttp.KFAHandler
SSAllergyIntolerance *satuSehatUsecaseHttp.AllergyIntoleranceHandler
SSCarePlan *satuSehatUsecaseHttp.CarePlanHandler
SSClinicalImpression *satuSehatUsecaseHttp.ClinicalImpressionHandler
SSComposition *satuSehatUsecaseHttp.CompositionHandler
SSCondition *satuSehatUsecaseHttp.ConditionHandler
SSDiagnosticReport *satuSehatUsecaseHttp.DiagnosticReportHandler
SSEncounter *satuSehatUsecaseHttp.EncounterHandler
SSEpisodeOfCare *satuSehatUsecaseHttp.EpisodeOfCareHandler
SSImagingStudy *satuSehatUsecaseHttp.ImagingStudyHandler
SSImmunization *satuSehatUsecaseHttp.ImmunizationHandler
SSMedication *satuSehatUsecaseHttp.MedicationHandler
SSMedicationDispense *satuSehatUsecaseHttp.MedicationDispenseHandler
SSMedicationRequest *satuSehatUsecaseHttp.MedicationRequestHandler
SSMedicationStatement *satuSehatUsecaseHttp.MedicationStatementHandler
SSObservation *satuSehatUsecaseHttp.ObservationHandler
SSProcedure *satuSehatUsecaseHttp.ProcedureHandler
SSQuestionnaireResponse *satuSehatUsecaseHttp.QuestionnaireResponseHandler
SSServiceRequest *satuSehatUsecaseHttp.ServiceRequestHandler
SSSpecimen *satuSehatUsecaseHttp.SpecimenHandler
}
// SetupRoutes mendaftarkan seluruh endpoint API secara dinamis berdasarkan
// modul-modul yang aktif (tidak nil) pada aplikasi.
func SetupRoutes(
engine *gin.Engine,
cfg *config.Config,
cacheManager *cache.Manager,
healthHandler *healthHttp.HealthHandler,
h *ModuleHandlers,
) {
// Handle 405 Method Not Allowed for better client feedback
engine.HandleMethodNotAllowed = true
// Health check endpoints - using consistent http.Status constants
engine.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"timestamp": time.Now().UTC(),
"service": "service-general",
"version": "1.0.0",
})
})
// Comprehensive health check with dependencies
engine.GET("/health/complete", healthHandler.HealthCheckComplete)
// Database health check
engine.GET("/health/database", healthHandler.HealthCheckDatabase)
// Redis/cache health check
engine.GET("/health/cache", healthHandler.HealthCheckCache)
// External services health check
engine.GET("/health/external", healthHandler.HealthCheckExternal)
// Minio service
engine.GET("/health/minio", healthHandler.HealthCheckMinio)
engine.POST("/health/minio/upload", healthHandler.TestUploadMinio)
// All databases health check
// engine.GET("/health/databases", healthHandler.HealthCheckAllDatabases)
// Readiness check
engine.GET("/ready", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ready",
"timestamp": time.Now().UTC(),
})
})
// Liveness check
engine.GET("/live", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "alive",
"timestamp": time.Now().UTC(),
})
})
// Redirect otomatis dari /swagger ke /swagger/index.html
engine.GET("/swagger", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/swagger/index.html")
})
// Endpoint untuk Swagger UI
engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// API v1 routes
v1 := engine.Group("/api/v1")
{
// Register auth routes (Public)
if h.Auth != nil {
h.Auth.RegisterRoutes(v1)
}
// Private routes (Membutuhkan Autentikasi)
protected := v1.Group("")
protected.Use(middleware.AuthMiddleware(cfg, cacheManager))
{
if h.Auth != nil {
h.Auth.RegisterProtectedRoutes(protected)
}
// --- Routes Modul Master ---
if h.RolePages != nil {
h.RolePages.RegisterRoutes(protected)
}
if h.RolePermission != nil {
h.RolePermission.RegisterRoutes(protected)
}
if h.RoleMaster != nil {
h.RoleMaster.RegisterRoutes(protected)
}
// --- Routes Modul BPJS VClaim ---
if h.Sep != nil {
h.Sep.RegisterRoutes(protected)
}
if h.Peserta != nil {
h.Peserta.RegisterRoutes(protected)
}
// --- Routes Modul BPJS Antrol ---
if h.Antrol != nil {
h.Antrol.RegisterRoutes(protected)
}
// --- Routes Modul BPJS Aplicares ---
if h.AplicareBed != nil {
h.AplicareBed.RegisterRoutes(protected)
}
// --- Routes Modul BPJS Apotek ---
if h.ApotekReference != nil {
h.ApotekReference.RegisterRoutes(protected)
}
// --- Routes Modul Satu Sehat ---
// Buat grup khusus untuk membatasi traffic hit API ke Kemenkes
satuSehatGroup := protected.Group("")
satuSehatGroup.Use(middleware.MemoryRateLimitMiddleware(5.0, 10)) // Max 5 RPS, Burst capacity 10
{
if h.SatuSehatAuth != nil {
h.SatuSehatAuth.RegisterRoutes(satuSehatGroup)
}
if h.SatuSehatPatient != nil {
h.SatuSehatPatient.RegisterRoutes(satuSehatGroup)
}
if h.SatuSehatPractitioner != nil {
h.SatuSehatPractitioner.RegisterRoutes(satuSehatGroup)
}
if h.SatuSehatOrganization != nil {
h.SatuSehatOrganization.RegisterRoutes(satuSehatGroup)
}
if h.SatuSehatLocation != nil {
h.SatuSehatLocation.RegisterRoutes(satuSehatGroup)
}
if h.SatuSehatKFA != nil {
h.SatuSehatKFA.RegisterRoutes(satuSehatGroup)
}
if h.SSAllergyIntolerance != nil {
h.SSAllergyIntolerance.RegisterRoutes(satuSehatGroup)
}
if h.SSCarePlan != nil {
h.SSCarePlan.RegisterRoutes(satuSehatGroup)
}
if h.SSClinicalImpression != nil {
h.SSClinicalImpression.RegisterRoutes(satuSehatGroup)
}
if h.SSComposition != nil {
h.SSComposition.RegisterRoutes(satuSehatGroup)
}
if h.SSCondition != nil {
h.SSCondition.RegisterRoutes(satuSehatGroup)
}
if h.SSDiagnosticReport != nil {
h.SSDiagnosticReport.RegisterRoutes(satuSehatGroup)
}
if h.SSEncounter != nil {
h.SSEncounter.RegisterRoutes(satuSehatGroup)
}
if h.SSEpisodeOfCare != nil {
h.SSEpisodeOfCare.RegisterRoutes(satuSehatGroup)
}
if h.SSImagingStudy != nil {
h.SSImagingStudy.RegisterRoutes(satuSehatGroup)
}
if h.SSImmunization != nil {
h.SSImmunization.RegisterRoutes(satuSehatGroup)
}
if h.SSMedication != nil {
h.SSMedication.RegisterRoutes(satuSehatGroup)
}
if h.SSMedicationDispense != nil {
h.SSMedicationDispense.RegisterRoutes(satuSehatGroup)
}
if h.SSMedicationRequest != nil {
h.SSMedicationRequest.RegisterRoutes(satuSehatGroup)
}
if h.SSMedicationStatement != nil {
h.SSMedicationStatement.RegisterRoutes(satuSehatGroup)
}
if h.SSObservation != nil {
h.SSObservation.RegisterRoutes(satuSehatGroup)
}
if h.SSProcedure != nil {
h.SSProcedure.RegisterRoutes(satuSehatGroup)
}
if h.SSQuestionnaireResponse != nil {
h.SSQuestionnaireResponse.RegisterRoutes(satuSehatGroup)
}
if h.SSServiceRequest != nil {
h.SSServiceRequest.RegisterRoutes(satuSehatGroup)
}
if h.SSSpecimen != nil {
h.SSSpecimen.RegisterRoutes(satuSehatGroup)
}
}
}
}
}
@@ -0,0 +1,123 @@
package servers
import (
"service/internal/infrastructure/cache"
"service/internal/infrastructure/config"
"service/internal/infrastructure/database"
"gorm.io/gorm"
"service/internal/auth"
antrol "service/internal/bpjs/antrol/reference"
aplicareBed "service/internal/bpjs/aplicare/bed"
apotekDpho "service/internal/bpjs/apotek/reference/dpho"
apotekPoli "service/internal/bpjs/apotek/reference/poli"
peserta "service/internal/bpjs/vclaim/peserta"
sep "service/internal/bpjs/vclaim/sep"
roleMaster "service/internal/master/role/master"
rolePages "service/internal/master/role/pages"
rolePermission "service/internal/master/role/permission"
satuSehatAuth "service/internal/satusehat/reference/auth"
satuSehatKfa "service/internal/satusehat/reference/kfa"
satuSehatLocation "service/internal/satusehat/reference/location"
satuSehatOrganization "service/internal/satusehat/reference/organization"
satuSehatPatient "service/internal/satusehat/reference/patient"
satuSehatPractitioner "service/internal/satusehat/reference/practitioner"
"service/internal/satusehat/usecase/allergyintolerance"
"service/internal/satusehat/usecase/careplan"
clinicalimpression "service/internal/satusehat/usecase/clinicalImpression"
"service/internal/satusehat/usecase/composition"
"service/internal/satusehat/usecase/condition"
"service/internal/satusehat/usecase/diagnosticreport"
"service/internal/satusehat/usecase/encounter"
"service/internal/satusehat/usecase/episodeofcare"
"service/internal/satusehat/usecase/imagingstudy"
"service/internal/satusehat/usecase/immunization"
"service/internal/satusehat/usecase/medication"
"service/internal/satusehat/usecase/medicationdispense"
"service/internal/satusehat/usecase/medicationrequest"
"service/internal/satusehat/usecase/medicationstatement"
"service/internal/satusehat/usecase/observation"
"service/internal/satusehat/usecase/procedure"
"service/internal/satusehat/usecase/questionnaireresponse"
"service/internal/satusehat/usecase/servicerequest"
"service/internal/satusehat/usecase/specimen"
)
// MasterServices menampung kumpulan service untuk domain Master Data
type MasterServices struct {
RolePages rolePages.Service
RolePermission rolePermission.Service
RoleMaster roleMaster.Service
}
// VClaimServices menampung kumpulan service untuk integrasi BPJS VClaim
type VClaimServices struct {
Sep sep.Service
Peserta peserta.Service
}
// AntrolServices menampung kumpulan service untuk integrasi BPJS Antrean RS
type AntrolServices struct {
Reference antrol.Service
}
// AplicaresServices menampung kumpulan service untuk integrasi BPJS Aplicares
type AplicaresServices struct {
Bed aplicareBed.Service
}
// ApotekServices menampung kumpulan service untuk integrasi BPJS Apotek
type ApotekServices struct {
DPHO apotekDpho.Service
Poli apotekPoli.Service
}
// SatuSehatServices menampung kumpulan service untuk integrasi Kemenkes Satu Sehat
type SatuSehatServices struct {
Auth satuSehatAuth.Service
Patient satuSehatPatient.Service
Practitioner satuSehatPractitioner.Service
Organization satuSehatOrganization.Service
Location satuSehatLocation.Service
KFA satuSehatKfa.Service
// Usecase Services
AllergyIntolerance allergyintolerance.Service
CarePlan careplan.Service
ClinicalImpression clinicalimpression.Service
Composition composition.Service
Condition condition.Service
DiagnosticReport diagnosticreport.Service
Encounter encounter.Service
EpisodeOfCare episodeofcare.Service
ImagingStudy imagingstudy.Service
Immunization immunization.Service
Medication medication.Service
MedicationDispense medicationdispense.Service
MedicationRequest medicationrequest.Service
MedicationStatement medicationstatement.Service
Observation observation.Service
Procedure procedure.Service
QuestionnaireResponse questionnaireresponse.Service
ServiceRequest servicerequest.Service
Specimen specimen.Service
}
// ServiceRegistry berfungsi sebagai Dependency Injection Container untuk transport HTTP.
// Struktur ini mencegah membengkaknya parameter pada saat inisialisasi API.
type ServiceRegistry struct {
Config *config.Config
DBManager database.Service
PrimaryDB *gorm.DB
CacheManager *cache.Manager
// Modul Aplikasi (Pisahkan berdasarkan Bounded Context)
AuthService auth.Service
Master *MasterServices
VClaim *VClaimServices
Antrol *AntrolServices
Aplicare *AplicaresServices
Apotek *ApotekServices
SatuSehat *SatuSehatServices
}
@@ -0,0 +1,266 @@
// internal/infrastructure/transport/http/servers/server.go
package servers
import (
"context" // PERBAIKAN: Tambahkan import untuk graceful shutdown
"fmt"
"log"
"net/http"
"time"
"service/internal/infrastructure/cache" // PERBAIKAN: Tambahkan import cache
"service/internal/infrastructure/config"
"service/internal/infrastructure/transport/http/middleware"
"service/internal/infrastructure/transport/http/routes"
antrolHttp "service/internal/infrastructure/transport/http/handlers/bpjs/antrol"
aplicareHttp "service/internal/infrastructure/transport/http/handlers/bpjs/aplicare"
apotekHttp "service/internal/infrastructure/transport/http/handlers/bpjs/apotek"
vclaimHttp "service/internal/infrastructure/transport/http/handlers/bpjs/vclaim"
authHttp "service/internal/infrastructure/transport/http/handlers/main/auth"
healthHttp "service/internal/infrastructure/transport/http/handlers/main/health"
roleHttp "service/internal/infrastructure/transport/http/handlers/main/master/roles"
satuSehatRefHttp "service/internal/infrastructure/transport/http/handlers/satusehat/reference"
satuSehatUsecaseHttp "service/internal/infrastructure/transport/http/handlers/satusehat/usecase"
// Inisialisasi pkg
"service/pkg/errors"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
type Server struct {
engine *gin.Engine
config *config.ServerRESTConfig
cacheConfig cache.CacheConfig // PERBAIKAN: Tambahkan cacheConfig
httpServer *http.Server // PERBAIKAN: Simpan instance http.Server untuk graceful shutdown
}
// NewHTTPServer membuat instance server HTTP baru
func NewHTTPServer(
restConfig *config.ServerRESTConfig,
registry *ServiceRegistry,
) *Server {
if !restConfig.Enabled {
log.Println("HTTP server is disabled in configuration.")
return &Server{
config: restConfig,
}
}
// Set mode Gin berdasarkan konfigurasi global
if registry.Config.Server.Mode == "release" {
gin.SetMode(gin.ReleaseMode)
}
engine := gin.New()
// Middleware untuk recovery dan penanganan error konsisten.
// Diletakkan paling awal untuk menangkap panic dari middleware/handler lain.
engine.Use(errors.CombinedMiddleware())
// Middleware lainnya
engine.Use(middleware.LoggingMiddleware())
engine.Use(middleware.SecurityMiddleware())
engine.Use(middleware.CORSMiddleware())
// PERBAIKAN: Berikan cacheManager ke RateLimitMiddleware
engine.Use(middleware.RateLimitMiddleware(registry.CacheManager))
// Inisialisasi ModuleHandlers secara dinamis berdasarkan fitur yang aktif
appHandlers := &routes.ModuleHandlers{}
if registry.AuthService != nil {
appHandlers.Auth = authHttp.NewAuthHandler(registry.AuthService)
}
// Inisialisasi Handlers Modul Master
if registry.Master != nil {
if registry.Master.RolePages != nil {
appHandlers.RolePages = roleHttp.NewRolPagesHandler(registry.Master.RolePages)
}
if registry.Master.RolePermission != nil {
appHandlers.RolePermission = roleHttp.NewRolPermissionHandler(registry.Master.RolePermission)
}
if registry.Master.RoleMaster != nil {
appHandlers.RoleMaster = roleHttp.NewRoleMasterHandler(registry.Master.RoleMaster)
}
}
// Inisialisasi Handlers Modul VClaim
if registry.VClaim != nil {
if registry.VClaim.Sep != nil {
appHandlers.Sep = vclaimHttp.NewSepHandler(registry.VClaim.Sep)
}
if registry.VClaim.Peserta != nil {
appHandlers.Peserta = vclaimHttp.NewPesertaHandler(registry.VClaim.Peserta)
}
}
// Inisialisasi Handlers Modul Antrol
if registry.Antrol != nil {
if registry.Antrol.Reference != nil {
appHandlers.Antrol = antrolHttp.NewAntrolHandler(registry.Antrol.Reference)
}
}
// Inisialisasi Handlers Modul Aplicares
if registry.Aplicare != nil {
if registry.Aplicare.Bed != nil {
appHandlers.AplicareBed = aplicareHttp.NewBedHandler(registry.Aplicare.Bed)
}
}
// Inisialisasi Handlers Modul Apotek
if registry.Apotek != nil {
if registry.Apotek.DPHO != nil && registry.Apotek.Poli != nil {
appHandlers.ApotekReference = apotekHttp.NewReferenceHandler(registry.Apotek.DPHO, registry.Apotek.Poli)
}
}
// Inisialisasi Handlers Modul Satu Sehat
if registry.SatuSehat != nil {
if registry.SatuSehat.Auth != nil {
appHandlers.SatuSehatAuth = satuSehatRefHttp.NewAuthHandler(registry.SatuSehat.Auth)
}
if registry.SatuSehat.Patient != nil {
appHandlers.SatuSehatPatient = satuSehatRefHttp.NewPatientHandler(registry.SatuSehat.Patient)
}
if registry.SatuSehat.Practitioner != nil {
appHandlers.SatuSehatPractitioner = satuSehatRefHttp.NewPractitionerHandler(registry.SatuSehat.Practitioner)
}
if registry.SatuSehat.Organization != nil {
appHandlers.SatuSehatOrganization = satuSehatRefHttp.NewOrganizationHandler(registry.SatuSehat.Organization)
}
if registry.SatuSehat.Location != nil {
appHandlers.SatuSehatLocation = satuSehatRefHttp.NewLocationHandler(registry.SatuSehat.Location)
}
if registry.SatuSehat.KFA != nil {
appHandlers.SatuSehatKFA = satuSehatRefHttp.NewKFAHandler(registry.SatuSehat.KFA)
}
// Inisialisasi Handlers Modul Satu Sehat Usecase
if registry.SatuSehat.AllergyIntolerance != nil {
appHandlers.SSAllergyIntolerance = satuSehatUsecaseHttp.NewAllergyIntoleranceHandler(registry.SatuSehat.AllergyIntolerance)
}
if registry.SatuSehat.CarePlan != nil {
appHandlers.SSCarePlan = satuSehatUsecaseHttp.NewCarePlanHandler(registry.SatuSehat.CarePlan)
}
if registry.SatuSehat.ClinicalImpression != nil {
appHandlers.SSClinicalImpression = satuSehatUsecaseHttp.NewClinicalImpressionHandler(registry.SatuSehat.ClinicalImpression)
}
if registry.SatuSehat.Composition != nil {
appHandlers.SSComposition = satuSehatUsecaseHttp.NewCompositionHandler(registry.SatuSehat.Composition)
}
if registry.SatuSehat.Condition != nil {
appHandlers.SSCondition = satuSehatUsecaseHttp.NewConditionHandler(registry.SatuSehat.Condition)
}
if registry.SatuSehat.DiagnosticReport != nil {
appHandlers.SSDiagnosticReport = satuSehatUsecaseHttp.NewDiagnosticReportHandler(registry.SatuSehat.DiagnosticReport)
}
if registry.SatuSehat.Encounter != nil {
appHandlers.SSEncounter = satuSehatUsecaseHttp.NewEncounterHandler(registry.SatuSehat.Encounter)
}
if registry.SatuSehat.EpisodeOfCare != nil {
appHandlers.SSEpisodeOfCare = satuSehatUsecaseHttp.NewEpisodeOfCareHandler(registry.SatuSehat.EpisodeOfCare)
}
if registry.SatuSehat.ImagingStudy != nil {
appHandlers.SSImagingStudy = satuSehatUsecaseHttp.NewImagingStudyHandler(registry.SatuSehat.ImagingStudy)
}
if registry.SatuSehat.Immunization != nil {
appHandlers.SSImmunization = satuSehatUsecaseHttp.NewImmunizationHandler(registry.SatuSehat.Immunization)
}
if registry.SatuSehat.Medication != nil {
appHandlers.SSMedication = satuSehatUsecaseHttp.NewMedicationHandler(registry.SatuSehat.Medication)
}
if registry.SatuSehat.MedicationDispense != nil {
appHandlers.SSMedicationDispense = satuSehatUsecaseHttp.NewMedicationDispenseHandler(registry.SatuSehat.MedicationDispense)
}
if registry.SatuSehat.MedicationRequest != nil {
appHandlers.SSMedicationRequest = satuSehatUsecaseHttp.NewMedicationRequestHandler(registry.SatuSehat.MedicationRequest)
}
if registry.SatuSehat.MedicationStatement != nil {
appHandlers.SSMedicationStatement = satuSehatUsecaseHttp.NewMedicationStatementHandler(registry.SatuSehat.MedicationStatement)
}
if registry.SatuSehat.Observation != nil {
appHandlers.SSObservation = satuSehatUsecaseHttp.NewObservationHandler(registry.SatuSehat.Observation)
}
if registry.SatuSehat.Procedure != nil {
appHandlers.SSProcedure = satuSehatUsecaseHttp.NewProcedureHandler(registry.SatuSehat.Procedure)
}
if registry.SatuSehat.QuestionnaireResponse != nil {
appHandlers.SSQuestionnaireResponse = satuSehatUsecaseHttp.NewQuestionnaireResponseHandler(registry.SatuSehat.QuestionnaireResponse)
}
if registry.SatuSehat.ServiceRequest != nil {
appHandlers.SSServiceRequest = satuSehatUsecaseHttp.NewServiceRequestHandler(registry.SatuSehat.ServiceRequest)
}
if registry.SatuSehat.Specimen != nil {
appHandlers.SSSpecimen = satuSehatUsecaseHttp.NewSpecimenHandler(registry.SatuSehat.Specimen)
}
}
// Inisialisasi Health Handler dengan dependensi yang benar
// Gunakan constructor baru yang menerima cache manager
healthHandler := healthHttp.NewHealthHandlerWithCache(registry.PrimaryDB, registry.CacheManager, registry.Config, registry.DBManager)
// Setup routes menggunakan handler yang sudah diinisialisasi
routes.SetupRoutes(
engine,
registry.Config,
registry.CacheManager,
healthHandler,
appHandlers,
)
return &Server{
engine: engine,
config: restConfig,
}
}
// RegisterSwagger mendaftarkan dokumentasi Swagger UI
func (s *Server) RegisterSwagger() {
if s.engine == nil {
log.Println("Cannot register Swagger: engine is nil (server disabled)")
return
}
s.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
// Start menjalankan server HTTP
func (s *Server) Start(globalConfig *config.ServerConfig) error {
if !s.config.Enabled {
log.Println("HTTP server is disabled, not starting.")
return nil
}
addr := fmt.Sprintf(":%d", s.config.Port)
log.Printf("Starting HTTP server on %s", addr)
// PERBAIKAN: Simpan instance server ke struct
s.httpServer = &http.Server{
Addr: addr,
Handler: s.engine,
ReadTimeout: time.Duration(globalConfig.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(globalConfig.WriteTimeout) * time.Second,
IdleTimeout: 60 * time.Second, // Good practice to have an idle timeout
}
return s.httpServer.ListenAndServe()
}
// PERBAIKAN: Tambahkan method untuk graceful shutdown
// Shutdown memberhentikan server HTTP dengan graceful shutdown
func (s *Server) Shutdown(ctx context.Context) error {
if s.httpServer == nil {
return nil
}
log.Println("Shutting down HTTP server...")
return s.httpServer.Shutdown(ctx)
}
// GetEngine mengembalikan instance Gin engine (terutama untuk testing)
func (s *Server) GetEngine() *gin.Engine {
return s.engine
}