From c3317dd9ee7069c6508ea149ca227c32fe2b400a Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Fri, 8 May 2026 10:06:43 +0700 Subject: [PATCH] update api --- config/dukcapil.go | 16 ++++--- infra/development.yaml | 4 +- internal/app/app.go | 2 +- internal/client/dukcapil_client.go | 52 +++++++++----------- internal/contract/dukcapil_contract.go | 16 +++---- internal/handler/dukcapil_handler.go | 66 ++++++++++++++++++-------- 6 files changed, 90 insertions(+), 66 deletions(-) diff --git a/config/dukcapil.go b/config/dukcapil.go index 9d99954..a446688 100644 --- a/config/dukcapil.go +++ b/config/dukcapil.go @@ -4,13 +4,15 @@ import "time" // Dukcapil holds configuration for the Dukcapil Face Recognition (1:N) API. type Dukcapil struct { - BaseURL string `mapstructure:"base_url"` - CustomerID string `mapstructure:"customer_id"` - Methode string `mapstructure:"methode"` - UserID string `mapstructure:"user_id"` - Password string `mapstructure:"password"` - DefaultIP string `mapstructure:"default_ip"` - TimeoutSecond int `mapstructure:"timeout_second"` + BaseURL string `mapstructure:"base_url"` + CustomerID string `mapstructure:"customer_id"` + Methode string `mapstructure:"methode"` + UserID string `mapstructure:"user_id"` + Password string `mapstructure:"password"` + DefaultIP string `mapstructure:"default_ip"` + TimeoutSecond int `mapstructure:"timeout_second"` + TransactionSource string `mapstructure:"transaction_source"` + Threshold string `mapstructure:"threshold"` } func (d *Dukcapil) Timeout() time.Duration { diff --git a/infra/development.yaml b/infra/development.yaml index 771575f..94ffda5 100644 --- a/infra/development.yaml +++ b/infra/development.yaml @@ -33,5 +33,7 @@ dukcapil: methode: "CALL_FN" user_id: "281020241202039900305241000011252" password: "Fjskdhv35$%" - default_ip: "10.160.86.53" + default_ip: "10.160.86.48" timeout_second: 30 + transaction_source: "eslogad" + threshold: "10" diff --git a/internal/app/app.go b/internal/app/app.go index f2b0c82..85f3bb8 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -33,7 +33,7 @@ func (a *App) Initialize(cfg *config.Config) error { dukcapilClient := client.NewDukcapilClient(cfg.Dukcapil) dukcapilService := service.NewDukcapilService(dukcapilClient) - dukcapilHandler := handler.NewDukcapilHandler(dukcapilService) + dukcapilHandler := handler.NewDukcapilHandler(dukcapilService, cfg) a.router = router.NewRouter( cfg, diff --git a/internal/client/dukcapil_client.go b/internal/client/dukcapil_client.go index e9edcb5..5680cd5 100644 --- a/internal/client/dukcapil_client.go +++ b/internal/client/dukcapil_client.go @@ -39,37 +39,31 @@ func (c *DukcapilClient) FaceMatch(ctx context.Context, req *contract.FaceMatchR return nil, errors.New("dukcapil: incomplete configuration") } + // Load PEM public key from file + pemBytes, err := os.ReadFile("infra/990030524100001.pem") + if err != nil { + return nil, fmt.Errorf("dukcapil: failed to read PEM file: %w", err) + } - ip := req.IP - if strings.TrimSpace(ip) == "" { - ip = c.cfg.DefaultIP - } + // Encrypt UserID and Password + encryptedUserID, err := util.EncryptWithPublicKey(c.cfg.UserID, pemBytes) + if err != nil { + return nil, fmt.Errorf("dukcapil: encrypt user_id: %w", err) + } + encryptedPassword, err := util.EncryptWithPublicKey(c.cfg.Password, pemBytes) + if err != nil { + return nil, fmt.Errorf("dukcapil: encrypt password: %w", err) + } - // Load PEM public key from file - pemBytes, err := os.ReadFile("infra/990030524100001.pem") - if err != nil { - return nil, fmt.Errorf("dukcapil: failed to read PEM file: %w", err) - } - - // Encrypt UserID and Password - encryptedUserID, err := util.EncryptWithPublicKey(c.cfg.UserID, pemBytes) - if err != nil { - return nil, fmt.Errorf("dukcapil: encrypt user_id: %w", err) - } - encryptedPassword, err := util.EncryptWithPublicKey(c.cfg.Password, pemBytes) - if err != nil { - return nil, fmt.Errorf("dukcapil: encrypt password: %w", err) - } - - body := contract.DukcapilFaceRequest{ - TransactionID: req.TransactionID, - TransactionSource: req.TransactionSource, - Threshold: req.Threshold, - Image: req.Image, - UserID: encryptedUserID, - Password: encryptedPassword, - IP: ip, - } + body := contract.DukcapilFaceRequest{ + TransactionID: req.TransactionID, + TransactionSource: req.TransactionSource, + Threshold: req.Threshold, + Image: req.Image, + UserID: encryptedUserID, + Password: encryptedPassword, + IP: req.IP, + } payload, err := json.Marshal(body) if err != nil { diff --git a/internal/contract/dukcapil_contract.go b/internal/contract/dukcapil_contract.go index fd30783..707f0ab 100644 --- a/internal/contract/dukcapil_contract.go +++ b/internal/contract/dukcapil_contract.go @@ -3,15 +3,15 @@ package contract // FaceMatchRequest is the inbound payload from clients of this service to // trigger a Dukcapil 1:N face recognition lookup. // -// Image must already be a base64 (no data:image prefix) representation of a -// jpg/png file. Threshold is forwarded to Dukcapil (1..20). IP is optional; -// when empty the configured default IP will be used. +// Only the image file is required from the client. All other parameters +// (transaction_id, transaction_source, threshold, ip) are generated or +// retrieved from configuration in the backend. type FaceMatchRequest struct { - TransactionID string `json:"transaction_id" validate:"required,max=20"` - TransactionSource string `json:"transaction_source" validate:"required,max=50"` - Threshold string `json:"threshold" validate:"required"` - Image string `json:"image" validate:"required"` - IP string `json:"ip,omitempty"` + TransactionID string `json:"transaction_id"` + TransactionSource string `json:"transaction_source"` + Threshold string `json:"threshold"` + Image string `json:"image"` + IP string `json:"ip"` } // DukcapilFaceRequest is the exact JSON body sent to the Dukcapil diff --git a/internal/handler/dukcapil_handler.go b/internal/handler/dukcapil_handler.go index efcbc9f..2a1b5a4 100644 --- a/internal/handler/dukcapil_handler.go +++ b/internal/handler/dukcapil_handler.go @@ -1,9 +1,13 @@ package handler import ( + "encoding/base64" + "fmt" + "io" "net/http" - "strings" + "time" + "go-backend-template/config" "go-backend-template/internal/constants" "go-backend-template/internal/contract" "go-backend-template/internal/logger" @@ -14,39 +18,61 @@ import ( type DukcapilHandler struct { dukcapilService DukcapilService + config *config.Config } -func NewDukcapilHandler(dukcapilService DukcapilService) *DukcapilHandler { - return &DukcapilHandler{dukcapilService: dukcapilService} +func NewDukcapilHandler(dukcapilService DukcapilService, cfg *config.Config) *DukcapilHandler { + return &DukcapilHandler{ + dukcapilService: dukcapilService, + config: cfg, + } } // FaceMatch handles POST /api/v1/dukcapil/face-match (1:N face recognition). +// Accepts only an image file via multipart form. All other parameters are +// generated or retrieved from configuration. func (h *DukcapilHandler) FaceMatch(c *gin.Context) { - var req contract.FaceMatchRequest - if err := c.ShouldBindJSON(&req); err != nil { - logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> request binding failed") - h.sendValidationError(c, "Invalid request body", constants.MalformedFieldErrorCode) + // Parse multipart form + file, err := c.FormFile("image") + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> failed to get image file") + h.sendValidationError(c, "image file is required", constants.MissingFieldErrorCode) return } - if strings.TrimSpace(req.TransactionID) == "" { - h.sendValidationError(c, "transaction_id is required", constants.MissingFieldErrorCode) + // Open the uploaded file + src, err := file.Open() + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> failed to open image file") + h.sendValidationError(c, "failed to read image file", constants.MalformedFieldErrorCode) return } - if strings.TrimSpace(req.TransactionSource) == "" { - h.sendValidationError(c, "transaction_source is required", constants.MissingFieldErrorCode) - return - } - if strings.TrimSpace(req.Threshold) == "" { - h.sendValidationError(c, "threshold is required", constants.MissingFieldErrorCode) - return - } - if strings.TrimSpace(req.Image) == "" { - h.sendValidationError(c, "image is required (base64-encoded)", constants.MissingFieldErrorCode) + defer src.Close() + + // Read file content + imageBytes, err := io.ReadAll(src) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> failed to read image bytes") + h.sendValidationError(c, "failed to read image file", constants.MalformedFieldErrorCode) return } - res, err := h.dukcapilService.FaceMatch(c.Request.Context(), &req) + // Convert to base64 + imageBase64 := base64.StdEncoding.EncodeToString(imageBytes) + + // Generate transaction_id (timestamp-based random ID) + transactionID := fmt.Sprintf("TXN%d", time.Now().UnixNano()) + + // Build request with config values + req := &contract.FaceMatchRequest{ + TransactionID: transactionID, + TransactionSource: h.config.Dukcapil.TransactionSource, + Threshold: h.config.Dukcapil.Threshold, + Image: imageBase64, + IP: h.config.Dukcapil.DefaultIP, + } + + res, err := h.dukcapilService.FaceMatch(c.Request.Context(), req) if err != nil { logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> upstream call failed") c.JSON(http.StatusBadGateway, &contract.ErrorResponse{