update api

This commit is contained in:
Aditya Siregar 2026-05-08 10:06:43 +07:00
parent 2c43e758f5
commit c3317dd9ee
6 changed files with 90 additions and 66 deletions

View File

@ -4,13 +4,15 @@ import "time"
// Dukcapil holds configuration for the Dukcapil Face Recognition (1:N) API. // Dukcapil holds configuration for the Dukcapil Face Recognition (1:N) API.
type Dukcapil struct { type Dukcapil struct {
BaseURL string `mapstructure:"base_url"` BaseURL string `mapstructure:"base_url"`
CustomerID string `mapstructure:"customer_id"` CustomerID string `mapstructure:"customer_id"`
Methode string `mapstructure:"methode"` Methode string `mapstructure:"methode"`
UserID string `mapstructure:"user_id"` UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"` Password string `mapstructure:"password"`
DefaultIP string `mapstructure:"default_ip"` DefaultIP string `mapstructure:"default_ip"`
TimeoutSecond int `mapstructure:"timeout_second"` TimeoutSecond int `mapstructure:"timeout_second"`
TransactionSource string `mapstructure:"transaction_source"`
Threshold string `mapstructure:"threshold"`
} }
func (d *Dukcapil) Timeout() time.Duration { func (d *Dukcapil) Timeout() time.Duration {

View File

@ -33,5 +33,7 @@ dukcapil:
methode: "CALL_FN" methode: "CALL_FN"
user_id: "281020241202039900305241000011252" user_id: "281020241202039900305241000011252"
password: "Fjskdhv35$%" password: "Fjskdhv35$%"
default_ip: "10.160.86.53" default_ip: "10.160.86.48"
timeout_second: 30 timeout_second: 30
transaction_source: "eslogad"
threshold: "10"

View File

@ -33,7 +33,7 @@ func (a *App) Initialize(cfg *config.Config) error {
dukcapilClient := client.NewDukcapilClient(cfg.Dukcapil) dukcapilClient := client.NewDukcapilClient(cfg.Dukcapil)
dukcapilService := service.NewDukcapilService(dukcapilClient) dukcapilService := service.NewDukcapilService(dukcapilClient)
dukcapilHandler := handler.NewDukcapilHandler(dukcapilService) dukcapilHandler := handler.NewDukcapilHandler(dukcapilService, cfg)
a.router = router.NewRouter( a.router = router.NewRouter(
cfg, cfg,

View File

@ -39,37 +39,31 @@ func (c *DukcapilClient) FaceMatch(ctx context.Context, req *contract.FaceMatchR
return nil, errors.New("dukcapil: incomplete configuration") 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 // Encrypt UserID and Password
if strings.TrimSpace(ip) == "" { encryptedUserID, err := util.EncryptWithPublicKey(c.cfg.UserID, pemBytes)
ip = c.cfg.DefaultIP 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 body := contract.DukcapilFaceRequest{
pemBytes, err := os.ReadFile("infra/990030524100001.pem") TransactionID: req.TransactionID,
if err != nil { TransactionSource: req.TransactionSource,
return nil, fmt.Errorf("dukcapil: failed to read PEM file: %w", err) Threshold: req.Threshold,
} Image: req.Image,
UserID: encryptedUserID,
// Encrypt UserID and Password Password: encryptedPassword,
encryptedUserID, err := util.EncryptWithPublicKey(c.cfg.UserID, pemBytes) IP: req.IP,
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,
}
payload, err := json.Marshal(body) payload, err := json.Marshal(body)
if err != nil { if err != nil {

View File

@ -3,15 +3,15 @@ package contract
// FaceMatchRequest is the inbound payload from clients of this service to // FaceMatchRequest is the inbound payload from clients of this service to
// trigger a Dukcapil 1:N face recognition lookup. // trigger a Dukcapil 1:N face recognition lookup.
// //
// Image must already be a base64 (no data:image prefix) representation of a // Only the image file is required from the client. All other parameters
// jpg/png file. Threshold is forwarded to Dukcapil (1..20). IP is optional; // (transaction_id, transaction_source, threshold, ip) are generated or
// when empty the configured default IP will be used. // retrieved from configuration in the backend.
type FaceMatchRequest struct { type FaceMatchRequest struct {
TransactionID string `json:"transaction_id" validate:"required,max=20"` TransactionID string `json:"transaction_id"`
TransactionSource string `json:"transaction_source" validate:"required,max=50"` TransactionSource string `json:"transaction_source"`
Threshold string `json:"threshold" validate:"required"` Threshold string `json:"threshold"`
Image string `json:"image" validate:"required"` Image string `json:"image"`
IP string `json:"ip,omitempty"` IP string `json:"ip"`
} }
// DukcapilFaceRequest is the exact JSON body sent to the Dukcapil // DukcapilFaceRequest is the exact JSON body sent to the Dukcapil

View File

@ -1,9 +1,13 @@
package handler package handler
import ( import (
"encoding/base64"
"fmt"
"io"
"net/http" "net/http"
"strings" "time"
"go-backend-template/config"
"go-backend-template/internal/constants" "go-backend-template/internal/constants"
"go-backend-template/internal/contract" "go-backend-template/internal/contract"
"go-backend-template/internal/logger" "go-backend-template/internal/logger"
@ -14,39 +18,61 @@ import (
type DukcapilHandler struct { type DukcapilHandler struct {
dukcapilService DukcapilService dukcapilService DukcapilService
config *config.Config
} }
func NewDukcapilHandler(dukcapilService DukcapilService) *DukcapilHandler { func NewDukcapilHandler(dukcapilService DukcapilService, cfg *config.Config) *DukcapilHandler {
return &DukcapilHandler{dukcapilService: dukcapilService} return &DukcapilHandler{
dukcapilService: dukcapilService,
config: cfg,
}
} }
// FaceMatch handles POST /api/v1/dukcapil/face-match (1:N face recognition). // 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) { func (h *DukcapilHandler) FaceMatch(c *gin.Context) {
var req contract.FaceMatchRequest // Parse multipart form
if err := c.ShouldBindJSON(&req); err != nil { file, err := c.FormFile("image")
logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> request binding failed") if err != nil {
h.sendValidationError(c, "Invalid request body", constants.MalformedFieldErrorCode) 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 return
} }
if strings.TrimSpace(req.TransactionID) == "" { // Open the uploaded file
h.sendValidationError(c, "transaction_id is required", constants.MissingFieldErrorCode) 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 return
} }
if strings.TrimSpace(req.TransactionSource) == "" { defer src.Close()
h.sendValidationError(c, "transaction_source is required", constants.MissingFieldErrorCode)
return // Read file content
} imageBytes, err := io.ReadAll(src)
if strings.TrimSpace(req.Threshold) == "" { if err != nil {
h.sendValidationError(c, "threshold is required", constants.MissingFieldErrorCode) logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> failed to read image bytes")
return h.sendValidationError(c, "failed to read image file", constants.MalformedFieldErrorCode)
}
if strings.TrimSpace(req.Image) == "" {
h.sendValidationError(c, "image is required (base64-encoded)", constants.MissingFieldErrorCode)
return 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 { if err != nil {
logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> upstream call failed") logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> upstream call failed")
c.JSON(http.StatusBadGateway, &contract.ErrorResponse{ c.JSON(http.StatusBadGateway, &contract.ErrorResponse{