package client import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "strings" "time" "go-backend-template/config" "go-backend-template/internal/contract" "go-backend-template/internal/logger" "go-backend-template/internal/util" ) // DukcapilClient performs HTTPS calls to the Dukcapil 1:N face recognition endpoint (CALL_FN). type DukcapilClient struct { cfg config.Dukcapil http *http.Client } func NewDukcapilClient(cfg config.Dukcapil) *DukcapilClient { return &DukcapilClient{ cfg: cfg, http: &http.Client{ Timeout: cfg.Timeout(), }, } } // FaceMatch performs a 1:N face match call. The image must already be base64 encoded. func (c *DukcapilClient) FaceMatch(ctx context.Context, req *contract.FaceMatchRequest) (*contract.DukcapilFaceResponse, error) { if c.cfg.BaseURL == "" || c.cfg.CustomerID == "" || c.cfg.Methode == "" { 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) } // 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: req.IP, } payload, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("dukcapil: marshal request: %w", err) } url := fmt.Sprintf("%s/%s/%s", strings.TrimRight(c.cfg.BaseURL, "/"), c.cfg.CustomerID, c.cfg.Methode, ) // Log Dukcapil payload and URL (including params if any) logger.FromContext(ctx).Infof("DukcapilClient::FaceMatch -> URL: %s", url) logger.FromContext(ctx).Infof("DukcapilClient::FaceMatch -> Payload: %s", string(payload)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("dukcapil: build request: %w", err) } httpReq.Header.Set("Accept", "application/json") httpReq.Header.Set("Content-Type", "application/json") start := time.Now() resp, err := c.http.Do(httpReq) if err != nil { return nil, fmt.Errorf("dukcapil: do request: %w", err) } defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("dukcapil: read response: %w", err) } logger.FromContext(ctx).Infof("DukcapilClient::FaceMatch -> status=%d duration=%s", resp.StatusCode, time.Since(start)) if resp.StatusCode >= 500 { return nil, fmt.Errorf("dukcapil: upstream status %d: %s", resp.StatusCode, string(respBytes)) } var out contract.DukcapilFaceResponse if err := json.Unmarshal(respBytes, &out); err != nil { return nil, fmt.Errorf("dukcapil: decode response: %w (body=%s)", err, string(respBytes)) } return &out, nil }