From bb5f3876ec30adc92773759ae5f993d23711d6e6 Mon Sep 17 00:00:00 2001 From: Meninjar Mulyono Date: Tue, 2 Sep 2025 19:14:05 +0700 Subject: [PATCH] perbaikan decript --- internal/handlers/vclaim/peserta/peserta.go | 213 ++++++++++++++------ internal/models/vclaim/peserta/peserta.go | 71 ++++--- internal/routes/v1/routes.go | 2 +- internal/services/bpjs/response.go | 71 ++++++- internal/services/bpjs/vclaimBridge.go | 93 ++++++--- 5 files changed, 320 insertions(+), 130 deletions(-) diff --git a/internal/handlers/vclaim/peserta/peserta.go b/internal/handlers/vclaim/peserta/peserta.go index e582782..709277d 100644 --- a/internal/handlers/vclaim/peserta/peserta.go +++ b/internal/handlers/vclaim/peserta/peserta.go @@ -1,4 +1,3 @@ - // Service: VClaim (vclaim) // Description: BPJS VClaim service for eligibility and SEP management @@ -6,14 +5,14 @@ package peserta import ( "context" - "strings" + "encoding/json" "net/http" + "strings" "time" "api-service/internal/config" "api-service/internal/models" - "api-service/internal/models/vclaim/peserta" - "api-service/internal/services/bpjs" + services "api-service/internal/services/bpjs" "api-service/pkg/logger" "github.com/gin-gonic/gin" @@ -21,6 +20,62 @@ import ( "github.com/google/uuid" ) +// PesertaData represents peserta information from BPJS +type PesertaData struct { + NoKartu string `json:"noKartu"` + NIK string `json:"nik"` + Nama string `json:"nama"` + Pisa string `json:"pisa"` + Sex string `json:"sex"` + TanggalLahir string `json:"tglLahir"` + TglCetakKartu string `json:"tglCetakKartu"` + TglTAT string `json:"tglTAT"` + TglTMT string `json:"tglTMT"` + StatusPeserta struct { + Kode string `json:"kode"` + Keterangan string `json:"keterangan"` + } `json:"statusPeserta"` + ProvUmum struct { + KdProvider string `json:"kdProvider"` + NmProvider string `json:"nmProvider"` + } `json:"provUmum"` + JenisPeserta struct { + Kode string `json:"kode"` + Keterangan string `json:"keterangan"` + } `json:"jenisPeserta"` + HakKelas struct { + Kode string `json:"kode"` + Keterangan string `json:"keterangan"` + } `json:"hakKelas"` + Umur struct { + UmurSekarang string `json:"umurSekarang"` + UmurSaatPelayanan string `json:"umurSaatPelayanan"` + } `json:"umur"` + Informasi struct { + Dinsos interface{} `json:"dinsos"` + ProlanisPRB string `json:"prolanisPRB"` + NoSKTM interface{} `json:"noSKTM"` + ESEP interface{} `json:"eSEP"` + } `json:"informasi"` + Cob struct { + NoAsuransi interface{} `json:"noAsuransi"` + NmAsuransi interface{} `json:"nmAsuransi"` + TglTMT interface{} `json:"tglTMT"` + TglTAT interface{} `json:"tglTAT"` + } `json:"cob"` + MR struct { + NoMR string `json:"noMR"` + NoTelepon string `json:"noTelepon"` + } `json:"mr,omitempty"` +} + +// PesertaResponse represents peserta API response +type PesertaResponse struct { + models.BaseResponse + Data *PesertaData `json:"data,omitempty"` + MetaData interface{} `json:"metaData,omitempty"` +} + // VClaimHandler handles VClaim BPJS services type VClaimHandler struct { service services.VClaimService @@ -46,16 +101,15 @@ func NewVClaimHandler(cfg VClaimHandlerConfig) *VClaimHandler { } } - // GetPesertaBynokartu godoc // @Summary Get PesertaBynokartu data // @Description Get participant eligibility information by card number // @Tags Peserta // @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param X-Request-ID header string false "Request ID for tracking" -// @Param nokartu path string true "nokartu" example("example_value") +// @Produce json +// @Security ApiKeyAuth +// @Param X-Request-ID header string false "Request ID for tracking" +// @Param nokartu path string true "nokartu" example("example_value") // @Success 200 {object} peserta.PesertaResponse "Successfully retrieved PesertaBynokartu data" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" @@ -73,25 +127,22 @@ func (h *VClaimHandler) GetPesertaBynokartu(c *gin.Context) { c.Header("X-Request-ID", requestID) } - h.logger.Info("Processing GetPesertaBynokartu request", map[string]interface{}{ "request_id": requestID, - "endpoint": "/peserta/:nokartu", - - "nokartu": c.Param("nokartu"), - + "nokartu": c.Param("nokartu"), + "tglsep": c.Param("tglsep"), }) - // Extract path parameters - + nokartu := c.Param("nokartu") + tglsep := c.Param("tglsep") if nokartu == "" { - + h.logger.Error("Missing required parameter nokartu", map[string]interface{}{ "request_id": requestID, }) - + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter nokartu", @@ -99,24 +150,22 @@ func (h *VClaimHandler) GetPesertaBynokartu(c *gin.Context) { }) return } - // Call service method - var response peserta.PesertaResponse - - endpoint := "/peserta/:nokartu" - + var response PesertaResponse + + endpoint := "/Peserta/nokartu/:nokartu/tglSEP/:tglsep" + endpoint = strings.Replace(endpoint, ":nokartu", nokartu, 1) - - err := h.service.Get(ctx, endpoint, &response) - + endpoint = strings.Replace(endpoint, ":tglsep", tglsep, 1) + + resp, err := h.service.GetRawResponse(ctx, endpoint) if err != nil { - h.logger.Error("Failed to get PesertaBynokartu", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) - + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", @@ -125,29 +174,50 @@ func (h *VClaimHandler) GetPesertaBynokartu(c *gin.Context) { return } + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &PesertaData{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if pesertaMap, exists := respMap["peserta"]; exists { + pesertaBytes, _ := json.Marshal(pesertaMap) + json.Unmarshal(pesertaBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + // Ensure response has proper fields response.Status = "success" response.RequestID = requestID c.JSON(http.StatusOK, response) } - - - - - - - - // GetPesertaBynik godoc // @Summary Get PesertaBynik data // @Description Get participant eligibility information by NIK // @Tags Peserta // @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param X-Request-ID header string false "Request ID for tracking" -// @Param nik path string true "nik" example("example_value") +// @Produce json +// @Security ApiKeyAuth +// @Param X-Request-ID header string false "Request ID for tracking" +// @Param nik path string true "nik" example("example_value") // @Success 200 {object} peserta.PesertaResponse "Successfully retrieved PesertaBynik data" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" @@ -165,25 +235,22 @@ func (h *VClaimHandler) GetPesertaBynik(c *gin.Context) { c.Header("X-Request-ID", requestID) } - h.logger.Info("Processing GetPesertaBynik request", map[string]interface{}{ "request_id": requestID, "endpoint": "/peserta/nik/:nik", - + "nik": c.Param("nik"), - }) - // Extract path parameters - + nik := c.Param("nik") if nik == "" { - + h.logger.Error("Missing required parameter nik", map[string]interface{}{ "request_id": requestID, }) - + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter nik", @@ -191,24 +258,21 @@ func (h *VClaimHandler) GetPesertaBynik(c *gin.Context) { }) return } - // Call service method - var response peserta.PesertaResponse - + var response PesertaResponse + endpoint := "/peserta/nik/:nik" - + endpoint = strings.Replace(endpoint, ":nik", nik, 1) - - err := h.service.Get(ctx, endpoint, &response) - + + resp, err := h.service.GetRawResponse(ctx, endpoint) if err != nil { - h.logger.Error("Failed to get PesertaBynik", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) - + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", @@ -217,16 +281,37 @@ func (h *VClaimHandler) GetPesertaBynik(c *gin.Context) { return } + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &PesertaData{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if pesertaMap, exists := respMap["peserta"]; exists { + pesertaBytes, _ := json.Marshal(pesertaMap) + json.Unmarshal(pesertaBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + // Ensure response has proper fields response.Status = "success" response.RequestID = requestID c.JSON(http.StatusOK, response) } - - - - - - - - diff --git a/internal/models/vclaim/peserta/peserta.go b/internal/models/vclaim/peserta/peserta.go index dc5bddb..9fde276 100644 --- a/internal/models/vclaim/peserta/peserta.go +++ b/internal/models/vclaim/peserta/peserta.go @@ -15,33 +15,50 @@ type PesertaRequest struct { // PesertaData represents peserta information from BPJS type PesertaData struct { - NoKartu string `json:"noKartu"` - NIK string `json:"nik"` - Nama string `json:"nama"` - Pisa string `json:"pisa"` - Sex string `json:"sex"` - TanggalLahir string `json:"tglLahir"` - TelephoneMsisdn string `json:"tglTAT"` - TelephoneAsat string `json:"tglTMT"` - KodeCabang string `json:"kdCabang"` - NamaCabang string `json:"nmCabang"` - KodeJenisPeserta string `json:"kdJnsPst"` - NamaJenisPeserta string `json:"nmJnsPst"` - KelasRawat string `json:"klsRawat"` - Status string `json:"statusPeserta"` - Aktif string `json:"aktif"` - KeteranganAktif string `json:"ketAktif"` - NoSKTM string `json:"noSKTM,omitempty"` - NoKTP string `json:"noKtp"` - Asuransi string `json:"asuransi,omitempty"` - CoB string `json:"cob,omitempty"` - TunggakanIuran string `json:"tglTunggak,omitempty"` - MR struct { - NoMR string `json:"noMR"` - NamaMR string `json:"nmMR"` - Sex string `json:"sex"` - TglLahir string `json:"tglLahir"` - TglMeninggal string `json:"tglMeninggal,omitempty"` + NoKartu string `json:"noKartu"` + NIK string `json:"nik"` + Nama string `json:"nama"` + Pisa string `json:"pisa"` + Sex string `json:"sex"` + TanggalLahir string `json:"tglLahir"` + TglCetakKartu string `json:"tglCetakKartu"` + TglTAT string `json:"tglTAT"` + TglTMT string `json:"tglTMT"` + StatusPeserta struct { + Kode string `json:"kode"` + Keterangan string `json:"keterangan"` + } `json:"statusPeserta"` + ProvUmum struct { + KdProvider string `json:"kdProvider"` + NmProvider string `json:"nmProvider"` + } `json:"provUmum"` + JenisPeserta struct { + Kode string `json:"kode"` + Keterangan string `json:"keterangan"` + } `json:"jenisPeserta"` + HakKelas struct { + Kode string `json:"kode"` + Keterangan string `json:"keterangan"` + } `json:"hakKelas"` + Umur struct { + UmurSekarang string `json:"umurSekarang"` + UmurSaatPelayanan string `json:"umurSaatPelayanan"` + } `json:"umur"` + Informasi struct { + Dinsos interface{} `json:"dinsos"` + ProlanisPRB string `json:"prolanisPRB"` + NoSKTM interface{} `json:"noSKTM"` + ESEP interface{} `json:"eSEP"` + } `json:"informasi"` + Cob struct { + NoAsuransi interface{} `json:"noAsuransi"` + NmAsuransi interface{} `json:"nmAsuransi"` + TglTMT interface{} `json:"tglTMT"` + TglTAT interface{} `json:"tglTAT"` + } `json:"cob"` + MR struct { + NoMR string `json:"noMR"` + NoTelepon string `json:"noTelepon"` } `json:"mr,omitempty"` } diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 0698df3..0b0d56c 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -75,7 +75,7 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { Validator: nil, }) pesertaGroup := v1.Group("/peserta") - pesertaGroup.GET("/peserta/:nokartu", pesertaHandler.GetPesertaBynokartu) + pesertaGroup.GET("/peserta/:nokartu/tglSEP/:tglsep", pesertaHandler.GetPesertaBynokartu) pesertaGroup.GET("/peserta/nik/:nik", pesertaHandler.GetPesertaBynik) // Sep routes diff --git a/internal/services/bpjs/response.go b/internal/services/bpjs/response.go index c169a2c..ab2fbb9 100644 --- a/internal/services/bpjs/response.go +++ b/internal/services/bpjs/response.go @@ -2,49 +2,110 @@ package services import ( helper "api-service/internal/helpers/bpjs" + "bytes" + "compress/gzip" "crypto/aes" "crypto/cipher" "crypto/sha256" "encoding/base64" "errors" - - lzstring "github.com/daku10/go-lz-string" + "io" + "log" ) +func min(a, b int) int { + if a < b { + return a + } + return b +} + // ResponseVclaim decrypts the encrypted response from VClaim API func ResponseVclaim(encrypted string, key string) (string, error) { + log.Println("ResponseVclaim: Starting decryption process") + log.Printf("ResponseVclaim: Encrypted string: %s", encrypted) + + // Pad the base64 string if needed + if len(encrypted)%4 != 0 { + padding := (4 - len(encrypted)%4) % 4 + for i := 0; i < padding; i++ { + encrypted += "=" + } + log.Printf("ResponseVclaim: Padded encrypted string: %s", encrypted) + } + cipherText, err := base64.StdEncoding.DecodeString(encrypted) if err != nil { + log.Printf("ResponseVclaim: Failed to decode base64: %v", err) return "", err } + log.Printf("ResponseVclaim: Base64 decoded successfully, length: %d", len(cipherText)) hash := sha256.Sum256([]byte(key)) block, err := aes.NewCipher(hash[:]) if err != nil { + log.Printf("ResponseVclaim: Failed to create AES cipher: %v", err) return "", err } if len(cipherText) < aes.BlockSize { + log.Println("ResponseVclaim: CipherText too short") return "", errors.New("cipherText too short") } iv := hash[:aes.BlockSize] if len(cipherText)%aes.BlockSize != 0 { + log.Println("ResponseVclaim: CipherText not multiple of block size") return "", errors.New("cipherText is not a multiple of the block size") } mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(cipherText, cipherText) + log.Println("ResponseVclaim: AES decryption completed") // cipherText, _ = pkcs7.Unpad(cipherText, aes.BlockSize) cipherText = helper.RemovePKCS7Padding(cipherText) - data, err := lzstring.DecompressFromEncodedURIComponent(string(cipherText)) - // data, err := helper.DecompressFromEncodedUriComponent(string(cipherText)) + log.Printf("ResponseVclaim: PKCS7 padding removed, length: %d", len(cipherText)) + + var data string + + // Try gzip decompression first + reader, err := gzip.NewReader(bytes.NewReader(cipherText)) if err != nil { - return "", err + log.Printf("ResponseVclaim: Gzip decompression failed: %v, trying LZ-string decompression", err) + // Try lz-string decompression using helper function + data, err := helper.StringDecrypt(key, string(cipherText)) + if err != nil || len(data) == 0 { + log.Printf("ResponseVclaim: Helper StringDecrypt failed or empty: %v, trying without decompression", err) + // Try without decompression + data = string(cipherText) + log.Printf("ResponseVclaim: Using decrypted data without decompression, data length: %d", len(data)) + } else { + log.Printf("ResponseVclaim: Helper StringDecrypt successful, data length: %d, data: %s", len(data), data[:min(100, len(data))]) + } + } else { + defer reader.Close() + decompressed, err := io.ReadAll(reader) + if err != nil { + log.Printf("ResponseVclaim: Failed to read gzip decompressed data: %v, trying LZ-string decompression", err) + // Try lz-string decompression using helper function + data, err := helper.StringDecrypt(key, string(cipherText)) + if err != nil || len(data) == 0 { + log.Printf("ResponseVclaim: Helper StringDecrypt failed or empty: %v, using data without decompression", err) + data = string(cipherText) + log.Printf("ResponseVclaim: Using decrypted data without decompression, data length: %d", len(data)) + } else { + log.Printf("ResponseVclaim: Helper StringDecrypt successful, data length: %d", len(data)) + } + } else { + data = string(decompressed) + log.Printf("ResponseVclaim: Gzip decompression successful, data length: %d", len(data)) + } } + log.Printf("ResponseVclaim: Final data length: %d, data: %s", len(data), data[:min(100, len(data))]) + log.Println("ResponseVclaim: Decryption process completed successfully") return data, nil } diff --git a/internal/services/bpjs/vclaimBridge.go b/internal/services/bpjs/vclaimBridge.go index 76de691..66b1f27 100644 --- a/internal/services/bpjs/vclaimBridge.go +++ b/internal/services/bpjs/vclaimBridge.go @@ -10,6 +10,7 @@ import ( "time" "api-service/internal/config" + "api-service/internal/models/vclaim/peserta" "github.com/mashingan/smapping" "github.com/rs/zerolog/log" @@ -198,41 +199,53 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) return finalResp, nil } - // Decrypt response - consID, secretKey, _, tstamp, _ := s.config.SetHeader() - respDecrypt, err := ResponseVclaim(respMentah.Response, consID+secretKey+tstamp) - if err != nil { - log.Error(). - Err(err). - Str("meta_code", respMentah.MetaData.Code). - Msg("Failed to decrypt response") - return nil, fmt.Errorf("failed to decrypt response: %w", err) - } - - log.Debug(). - Str("encrypted_length", fmt.Sprintf("%d bytes", len(respMentah.Response))). - Str("decrypted_length", fmt.Sprintf("%d bytes", len(respDecrypt))). - Msg("Response decrypted successfully") - - // Unmarshal decrypted response - if respDecrypt != "" { - if err := json.Unmarshal([]byte(respDecrypt), &finalResp.Response); err != nil { - // If JSON unmarshal fails, store as string - log.Warn(). + // Try to unmarshal response as JSON first (in case it's not encrypted) + var tempResp interface{} + if json.Unmarshal([]byte(respMentah.Response), &tempResp) == nil { + log.Debug().Msg("Response is valid JSON, not encrypted") + finalResp.Response = tempResp + } else { + log.Debug().Msg("Response is not valid JSON, trying to decrypt") + // Decrypt response + consID, secretKey, _, tstamp, _ := s.config.SetHeader() + respDecrypt, err := ResponseVclaim(respMentah.Response, consID+secretKey+tstamp) + if err != nil { + log.Error(). Err(err). - Msg("Failed to unmarshal decrypted response, storing as string") - finalResp.Response = respDecrypt - } else { - log.Debug().Msg("Decrypted response unmarshaled successfully") + Str("meta_code", respMentah.MetaData.Code). + Msg("Failed to decrypt response") + return nil, fmt.Errorf("failed to decrypt response: %w", err) + } - // Use gjson to extract and log some metadata from the response if it's JSON - if jsonBytes, err := json.Marshal(finalResp.Response); err == nil { - jsonStr := string(jsonBytes) - // Extract some common fields using gjson - if metaCode := gjson.Get(jsonStr, "metaData.code"); metaCode.Exists() { - log.Info(). - Str("response_meta_code", metaCode.String()). - Msg("Final response metadata") + log.Debug(). + Str("encrypted_length", fmt.Sprintf("%d bytes", len(respMentah.Response))). + Str("decrypted_length", fmt.Sprintf("%d bytes", len(respDecrypt))). + Msg("Response decrypted successfully") + + log.Debug(). + Str("decrypted_data", respDecrypt). + Msg("Decrypted data") + + // Unmarshal decrypted response + if respDecrypt != "" { + if err := json.Unmarshal([]byte(respDecrypt), &finalResp.Response); err != nil { + // If JSON unmarshal fails, store as string + log.Warn(). + Err(err). + Msg("Failed to unmarshal decrypted response, storing as string") + finalResp.Response = respDecrypt + } else { + log.Debug().Msg("Decrypted response unmarshaled successfully") + + // Use gjson to extract and log some metadata from the response if it's JSON + if jsonBytes, err := json.Marshal(finalResp.Response); err == nil { + jsonStr := string(jsonBytes) + // Extract some common fields using gjson + if metaCode := gjson.Get(jsonStr, "metaData.code"); metaCode.Exists() { + log.Info(). + Str("response_meta_code", metaCode.String()). + Msg("Final response metadata") + } } } } @@ -410,6 +423,20 @@ func mapToResult(resp *ResponDTOVclaim, result interface{}) error { return fmt.Errorf("failed to unmarshal to result: %w", err) } + // Handle BPJS peserta response structure + if pesertaResp, ok := result.(*peserta.PesertaResponse); ok { + if resp.Response != nil { + if responseMap, ok := resp.Response.(map[string]interface{}); ok { + if pesertaMap, ok := responseMap["peserta"]; ok { + pesertaBytes, _ := json.Marshal(pesertaMap) + var pd peserta.PesertaData + json.Unmarshal(pesertaBytes, &pd) + pesertaResp.Data = &pd + } + } + } + } + return nil }