@@ -5,8 +5,12 @@ package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"api-service/internal/config"
@@ -48,6 +52,224 @@ func NewPesertaHandler(cfg PesertaHandlerConfig) *PesertaHandler {
}
}
// min returns the minimum of two integers
func min ( a , b int ) int {
if a < b {
return a
}
return b
}
// cleanResponse removes invalid characters and BOM from the response string
func cleanResponse ( resp string ) string {
// Remove UTF-8 BOM
// Konversi string ke byte slice untuk pengecekan BOM
data := [ ] byte ( resp )
// Cek dan hapus semua jenis representasi UTF-8 BOM
// 1. Byte sequence: EF BB BF
if len ( data ) >= 3 && data [ 0 ] == 0xEF && data [ 1 ] == 0xBB && data [ 2 ] == 0xBF {
data = data [ 3 : ]
}
// 2. Unicode character: U+FEFF
if len ( data ) >= 3 && data [ 0 ] == 0xEF && data [ 1 ] == 0xBB && data [ 2 ] == 0xBF {
data = data [ 3 : ]
}
// 3. Zero Width No-Break Space (Unicode)
if len ( data ) >= 3 && data [ 0 ] == 0xEF && data [ 1 ] == 0xBB && data [ 2 ] == 0xBF {
data = data [ 3 : ]
}
// 4. Representasi heksadesimal lainnya
if len ( data ) >= 3 && data [ 0 ] == 0xEF && data [ 1 ] == 0xBB && data [ 2 ] == 0xBF {
data = data [ 3 : ]
}
// Konversi kembali ke string
resp = string ( data )
// Hapus karakter null
// Hapus semua karakter kontrol ASCII (0-31) kecuali whitespace yang valid
controlChars := [ ] string {
"\x00" , // Null character
"\x01" , // Start of Heading
"\x02" , // Start of Text
"\x03" , // End of Text
"\x04" , // End of Transmission (EOT)
"\x05" , // Enquiry
"\x06" , // Acknowledge
"\x07" , // Bell
"\x08" , // Backspace
"\x0B" , // Vertical Tab
"\x0C" , // Form Feed
"\x0E" , // Shift Out
"\x0F" , // Shift In
"\x10" , // Data Link Escape
"\x11" , // Device Control 1
"\x12" , // Device Control 2
"\x13" , // Device Control 3
"\x14" , // Device Control 4
"\x15" , // Negative Acknowledge
"\x16" , // Synchronous Idle
"\x17" , // End of Transmission Block
"\x18" , // Cancel
"\x19" , // End of Medium
"\x1A" , // Substitute
"\x1B" , // Escape
"\x1C" , // File Separator
"\x1D" , // Group Separator
"\x1E" , // Record Separator
"\x1F" , // Unit Separator
}
for _ , char := range controlChars {
resp = strings . ReplaceAll ( resp , char , "" )
}
// Hapus karakter invalid termasuk backtick
invalidChars := [ ] string {
"¢" , // Cent sign
"\u00a2" , // Cent sign Unicode
"\u0080" , // Control character
"`" , // Backtick
"´ " , // Acute accent
"‘ " , // Left single quote
"’ " , // Right single quote
"“" , // Left double quote
"”" , // Right double quote
}
for _ , char := range invalidChars {
resp = strings . ReplaceAll ( resp , char , "" )
}
// Gunakan buffer pool untuk efisiensi memori
var bufPool = sync . Pool {
New : func ( ) interface { } {
return & strings . Builder { }
} ,
}
buf := bufPool . Get ( ) . ( * strings . Builder )
defer func ( ) {
buf . Reset ( )
bufPool . Put ( buf )
} ( )
// Definisikan karakter yang diperbolehkan
allowedChars := map [ rune ] bool {
'\n' : true , '\r' : true , '\t' : true ,
// Tambahkan karakter non-ASCII yang diperbolehkan jika adafalse
// Contoh:
// Latin-1 Supplement
// ASCII printable (32-126) kecuali backtick (96)
'!' : true , '"' : true , '#' : true , '$' : true , '%' : true , '&' : true ,
'\'' : true , '(' : true , ')' : true , '*' : true , '+' : true , ',' : true ,
'-' : true , '.' : true , '/' : true , '0' : true , '1' : true , '2' : true ,
'3' : true , '4' : true , '5' : true , '6' : true , '7' : true , '8' : true ,
'9' : true , ':' : true , ';' : true , '<' : true , '=' : true , '>' : true ,
'?' : true , '@' : true , 'A' : true , 'B' : true , 'C' : true , 'D' : true ,
'E' : true , 'F' : true , 'G' : true , 'H' : true , 'I' : true , 'J' : true ,
'K' : true , 'L' : true , 'M' : true , 'N' : true , 'O' : true , 'P' : true ,
'Q' : true , 'R' : true , 'S' : true , 'T' : true , 'U' : true , 'V' : true ,
'W' : true , 'X' : true , 'Y' : true , 'Z' : true , '[' : true , '\\' : true ,
']' : true , '^' : true , '_' : true , 'a' : true , 'b' : true , 'c' : true ,
'd' : true , 'e' : true , 'f' : true , 'g' : true , 'h' : true , 'i' : true ,
'j' : true , 'k' : true , 'l' : true , 'm' : true , 'n' : true , 'o' : true ,
'p' : true , 'q' : true , 'r' : true , 's' : true , 't' : true , 'u' : true ,
'v' : true , 'w' : true , 'x' : true , 'y' : true , 'z' : true , '{' : true ,
'|' : true , '}' : true , '~' : true ,
// Latin-1 Supplement
'¡' : true , '¢' : true , '£' : true , '¤' : true , '¥' : true , '¦' : true ,
'§' : true , '¨' : true , '©' : true , 'ª' : true , '«' : true , '¬' : true ,
'®' : true , '¯' : true , '°' : true , '±' : true , '²' : true , '³' : true ,
'´ ' : true , 'µ' : true , '¶' : true , '·' : true , '¸ ' : true , '¹' : true ,
'º' : true , '»' : true , '¼' : true , '½' : true , '¾' : true , '¿' : true ,
// Huruf Latin dengan diakritik (Lowercase)
'á' : true , 'é' : true , 'í' : true , 'ó' : true , 'ú' : true , 'ý' : true , 'þ' : true ,
'à' : true , 'è' : true , 'ì' : true , 'ò' : true , 'ù' : true ,
'â' : true , 'ê' : true , 'î' : true , 'ô' : true , 'û' : true ,
'ä' : true , 'ë' : true , 'ï' : true , 'ö' : true , 'ü' : true , 'ÿ' : true ,
'ã' : true , 'õ' : true , 'ñ' : true , 'ç' : true ,
'ā' : true , 'ē' : true , 'ī' : true , 'ō' : true , 'ū' : true ,
'ă' : true , 'đ' : true , 'ħ' : true , 'ij' : true , 'ĸ' : true , 'ł' : true ,
'ŋ' : true , 'œ' : true , 'ŧ' : true , 'ß' : true ,
// Huruf Latin dengan diakritik (Uppercase)
'Á' : true , 'É' : true , 'Í' : true , 'Ó' : true , 'Ú' : true , 'Ý' : true , 'Þ' : true ,
'À' : true , 'È' : true , 'Ì' : true , 'Ò' : true , 'Ù' : true ,
'Â' : true , 'Ê' : true , 'Î' : true , 'Ô' : true , 'Û' : true ,
'Ä' : true , 'Ë' : true , 'Ï' : true , 'Ö' : true , 'Ü' : true ,
'Ã' : true , 'Õ' : true , 'Ñ' : true , 'Ç' : true ,
'Ā' : true , 'Ē' : true , 'Ī' : true , 'Ō' : true , 'Ū' : true ,
'Ă' : true , 'Đ' : true , 'Ħ' : true , 'IJ' : true , 'Ł' : true ,
'Ŋ' : true , 'Œ' : true , 'Ŧ' : true , 'ẞ' : true ,
// Karakter Nordik dan lainnya
'Å' : true , 'å' : true , 'Æ' : true , 'æ' : true , 'Ø' : true , 'ø' : true ,
'ſ ' : true , 'ʼn' : true , 'ŀ' : true ,
// Tanda baca dan simbol matematika
'‐ ' : true , '– ' : true , '—' : true , '―' : true , '‖' : true , '‗' : true ,
'†' : true , '‡' : true , '•' : true , '‣' : true , '․ ' : true , '‥' : true ,
'…' : true , '‧' : true , '‰' : true , '′ ' : true , '″' : true , '‴' : true ,
'‵ ' : true , '‶' : true , '‷' : true , '‸' : true , '‹ ' : true , '› ' : true ,
'※' : true ,
// Simbol mata uang (hanya yang umum)
'€' : true , '₹' : true ,
// Karakter lain yang mungkin diperlukan
}
// Filter karakter menggunakan buffer pool
for _ , r := range resp {
if r < 128 || allowedChars [ r ] {
buf . WriteRune ( r )
}
}
// Trim whitespace
result := strings . TrimSpace ( buf . String ( ) )
return result
}
// extractCode extracts the code field from metaData using reflection
func extractCode ( metaData interface { } ) interface { } {
v := reflect . ValueOf ( metaData )
switch v . Kind ( ) {
case reflect . Struct :
codeField := v . FieldByName ( "Code" )
if codeField . IsValid ( ) {
return codeField . Interface ( )
}
case reflect . Map :
if m , ok := metaData . ( map [ string ] interface { } ) ; ok {
return m [ "code" ]
}
case reflect . String :
var metaMap map [ string ] interface { }
if err := json . Unmarshal ( [ ] byte ( metaData . ( string ) ) , & metaMap ) ; err == nil {
return metaMap [ "code" ]
}
}
return nil
}
// parseHTTPStatusCode extracts HTTP status code from error message
func parseHTTPStatusCode ( errMsg string ) int {
if strings . Contains ( errMsg , "HTTP error:" ) {
parts := strings . Split ( errMsg , "HTTP error:" )
if len ( parts ) > 1 {
statusPart := strings . TrimSpace ( parts [ 1 ] )
if statusCode , err := strconv . Atoi ( strings . Fields ( statusPart ) [ 0 ] ) ; err == nil {
return statusCode
}
}
}
return 500 // Default to internal server error
}
func ( h * PesertaHandler ) isValidJSON ( str string ) bool {
var js interface { }
return json . Unmarshal ( [ ] byte ( str ) , & js ) == nil
}
// GetBynik godoc
// @Summary Get Bynik data
// @Description Get participant eligibility information by NIK
@@ -64,6 +286,9 @@ func NewPesertaHandler(cfg PesertaHandlerConfig) *PesertaHandler {
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /Peserta/nik/:nik [get]
func ( h * PesertaHandler ) GetBynik ( c * gin . Context ) {
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , 30 * time . Second )
defer cancel ( )
// Generate request ID if not present
requestID := c . GetHeader ( "X-Request-ID" )
if requestID == "" {
@@ -87,20 +312,21 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) {
}
// Note: dbConn is available for future database operations (e.g., caching, logging)
_ = dbConn // Prevent unused variable warning
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , 30 * time . Second )
defer cancel ( )
// Context Paramaeter
now := time . Now ( )
dateStr := now . Format ( "2006-01-02" )
fmt . Println ( "Date (YYYY-MM-DD):" , dateStr )
h . logger . Info ( "Processing GetBynik request" , map [ string ] interface { } {
"request_id" : requestID ,
"endpoint" : "/p eserta/nik/:nik" ,
"nik" : c . Param ( "nik" ) ,
"endpoint" : "/P eserta/nik/:nik/tglSEP/" + dateStr ,
"nik" : c . Param ( "nik" ) ,
} )
// Extract path parameters
nik := c . Param ( "nik" )
if nik == "" {
if nik == "" || nik == ":nik" {
h . logger . Error ( "Missing required parameter nik" , map [ string ] interface { } {
"request_id" : requestID ,
@@ -108,16 +334,14 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) {
c . JSON ( http . StatusBadRequest , models . ErrorResponseBpjs {
Status : "error" ,
Message : "Missing required parameter nik " ,
Message : "Parameter NIK Masih Kosong / Isi Dahulu NIK! " ,
RequestID : requestID ,
} )
return
}
// Call service method
var response peserta . PesertaResponse
endpoint := "/p eserta/nik/:nik"
endpoint := "/P eserta/nik/:nik/tglSEP/" + dateStr
endpoint = strings . Replace ( endpoint , ":nik" , nik , 1 )
@@ -168,7 +392,28 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) {
} )
} else {
json . Unmarshal ( [ ] byte ( decryptedResp ) , response . Data )
// Clean the decrypted response
cleanedResp := cleanResponse ( decryptedResp )
if h . isValidJSON ( cleanedResp ) {
// Unmarshal kembali setelah dibersihkan
err = json . Unmarshal ( [ ] byte ( cleanedResp ) , response . Data )
if err != nil {
h . logger . Warn ( "Failed to unmarshal decrypted response" , map [ string ] interface { } {
"error" : err . Error ( ) ,
"request_id" : requestID ,
"response_preview" : cleanedResp [ : min ( 100 , len ( cleanedResp ) ) ] , // Log first 100 chars for debugging
} )
// Set Data to nil if unmarshal fails to avoid sending empty struct
response . Data = nil
}
} else {
h . logger . Warn ( "Invalid JSON in data, storing as string" , map [ string ] interface { } {
"request_id" : requestID ,
"response" : cleanedResp ,
} )
response . Data . RawResponse = cleanedResp
}
}
} else if respMap , ok := resp . Response . ( map [ string ] interface { } ) ; ok {
// Response is already unmarshaled JSON
@@ -186,7 +431,15 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) {
// Ensure response has proper fields
response . Status = "success"
response . RequestID = requestID
c . JSON ( http . StatusOK , response )
// Ambil status code dari metaData.code
var statusCode int
code := extractCode ( response . MetaData )
if code != nil {
statusCode = models . GetStatusCodeFromMeta ( code )
} else {
statusCode = 200
}
c . JSON ( statusCode , response )
}
// GetBynokartu godoc
@@ -215,17 +468,37 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) {
c . Header ( "X-Request-ID" , requestID )
}
// Get database connection
dbConn , err := h . db . GetDB ( "postgres_satudata" )
if err != nil {
h . logger . Error ( "Database connection failed" , map [ string ] interface { } {
"error" : err . Error ( ) ,
"request_id" : requestID ,
} )
c . JSON ( http . StatusInternalServerError , models . ErrorResponseBpjs {
Status : "error" ,
Message : "Database connection failed" ,
RequestID : requestID ,
} )
return
}
// Note: dbConn is available for future database operations (e.g., caching, logging)
_ = dbConn // Prevent unused variable warning
// Context Paramaeter
now := time . Now ( )
dateStr := now . Format ( "2006-01-02" )
fmt . Println ( "Date (YYYY-MM-DD):" , dateStr )
h . logger . Info ( "Processing GetBynokartu request" , map [ string ] interface { } {
"request_id" : requestID ,
"endpoint" : "/p eserta/:nokartu" ,
"nokartu" : c . Param ( "nokartu" ) ,
"endpoint" : "/P eserta/nokartu/:nokartu/tglSEP/" + dateStr ,
"nik" : c . Param ( "nokartu" ) ,
} )
// Extract path parameters
nokartu := c . Param ( "nokartu" )
if nokartu == "" {
if nokartu == "" || nokartu == ":nokartu" {
h . logger . Error ( "Missing required parameter nokartu" , map [ string ] interface { } {
"request_id" : requestID ,
@@ -233,24 +506,36 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) {
c . JSON ( http . StatusBadRequest , models . ErrorResponseBpjs {
Status : "error" ,
Message : "Missing required parameter nok artu" ,
Message : "Parameter Nomor Kartu Bpjs Masih Kosong / Isi Dahulu Nomor K artu! " ,
RequestID : requestID ,
} )
return
}
// Call service method
var response peserta . PesertaResponse
endpoint := "/p eserta/:nokartu"
endpoint := "/P eserta/nokartu/:nokartu/tglSEP/" + dateStr
endpoint = strings . Replace ( endpoint , ":nokartu" , nokartu , 1 )
resp , err := h . service . GetRawResponse ( ctx , endpoint )
if err != nil {
// Check if error message contains 404 status code
if strings . Contains ( err . Error ( ) , "HTTP error: 404" ) {
h . logger . Error ( "ByNoKartu not found" , map [ string ] interface { } {
"error" : err . Error ( ) ,
"request_id" : requestID ,
} )
h . logger . Error ( "Failed to get Bynokartu" , map [ string ] interface { } {
c . JSON ( http . StatusNotFound , models . ErrorResponseBpjs {
Status : "error" ,
Message : "ByNoKartu not found" ,
RequestID : requestID ,
} )
return
}
h . logger . Error ( "Failed to get ByNoKartu" , map [ string ] interface { } {
"error" : err . Error ( ) ,
"request_id" : requestID ,
} )
@@ -279,7 +564,18 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) {
} )
} else {
json . Unmarshal ( [ ] byte ( decryptedResp ) , response . Data )
// Clean the decrypted response
cleanedResp := cleanResponse ( decryptedResp )
err = json . Unmarshal ( [ ] byte ( cleanedResp ) , response . Data )
if err != nil {
h . logger . Warn ( "Failed to unmarshal decrypted response" , map [ string ] interface { } {
"error" : err . Error ( ) ,
"request_id" : requestID ,
"response_preview" : cleanedResp [ : min ( 100 , len ( cleanedResp ) ) ] , // Log first 100 chars for debugging
} )
// Set Data to nil if unmarshal fails to avoid sending empty struct
response . Data = nil
}
}
} else if respMap , ok := resp . Response . ( map [ string ] interface { } ) ; ok {
// Response is already unmarshaled JSON
@@ -297,30 +593,13 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) {
// Ensure response has proper fields
response . Status = "success"
response . RequestID = requestID
c . JSON ( http . StatusOK , response )
}
// Enhanced error handling
func ( h * PesertaHandler ) logAndRespondError ( c * gin . Context , message string , err error , statusCode int ) {
requestID := c . GetHeader ( "X-Request-ID" )
h . logger . Error ( message , map [ string ] interface { } {
"error" : err . Error ( ) ,
"status_code" : statusCode ,
"request_id" : requestID ,
} )
h . respondError ( c , message , err , statusCode )
}
func ( h * PesertaHandler ) respondError ( c * gin . Context , message string , err error , statusCode int ) {
requestID := c . GetHeader ( "X-Request-ID" )
errorMessage := message
if gin . Mode ( ) == gin . ReleaseMode {
errorMessage = "Internal server error"
// Ambil status code dari metaData.code
var statusCode int
code := extractCode ( response . MetaData )
if code != nil {
statusCode = models . GetStatusCodeFromMeta ( code )
} else {
statusCode = 200
}
c . JSON ( statusCode , models . ErrorResponseBpjs {
Status : "error" ,
Message : errorMessage ,
RequestID : requestID ,
} )
c . JSON ( statusCode , response )
}