Merge pull request 'connect: auditclient, httpclient, policyclient aus common v0.1.0' (#388) from refactor/connect-common into main
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Connect-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s

This commit was merged in pull request #388.
This commit is contained in:
patrick committed 2026-10-28 10:41:39 +01:00
commit 95582ad09d
33 files changed
+61 -399

No files matched your search

+5 -2
View File
@@ -4,12 +4,15 @@ go 1.25.0
toolchain go1.25.13
require github.com/jackc/pgx/v5 v5.9.2
require (
gitea.perlbach24.de/scripte/nexarch/common v0.1.0
github.com/jackc/pgx/v5 v5.9.2
golang.org/x/text v0.29.0
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
+2
View File
@@ -1,3 +1,5 @@
gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY=
gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -12,7 +12,7 @@ import (
"fmt"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
type Client struct {
@@ -1,71 +0,0 @@
// Package auditclient ist ein schlanker HTTP-Client fuer Core AUD-08
// (internal/audit, POST /audit/ingest). Connect ist ein physisch
// getrenntes Go-Modul und kann Cores internal/audit daher nicht direkt
// importieren — nur ueber dessen HTTP-Schnittstelle. Gleiches Muster wie
// archive/internal/auditclient.
package auditclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
)
type Client struct {
BaseURL string
ServiceToken string
HTTPClient *http.Client
}
func New(baseURL, serviceToken string) *Client {
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()}
}
type ingestRequest struct {
TenantSlug string `json:"tenant_slug"`
Actor string `json:"actor"`
Action string `json:"action"`
Target string `json:"target"`
Metadata map[string]any `json:"metadata"`
}
// Ingest meldet EIN Audit-Ereignis an Core. Jeder Fehler (Transport,
// Timeout, unerwarteter Status) wird als Fehler zurueckgegeben — der
// Aufrufer entscheidet, ob/wie er das nachbehandelt (siehe
// connect/internal/audittrail fuer den ausfalltoleranten Wrapper).
func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error {
body, err := json.Marshal(ingestRequest{
TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata,
})
if err != nil {
return fmt.Errorf("auditclient: request kodieren: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("auditclient: request bauen: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Service-Token", c.ServiceToken)
resp, err := c.httpClient().Do(req)
if err != nil {
return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode)
}
return nil
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return httpclient.Default()
}
@@ -1,57 +0,0 @@
package auditclient
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestIngest_SendsTenantSlugAndServiceToken(t *testing.T) {
var gotBody ingestRequest
var gotToken string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotToken = r.Header.Get("X-Service-Token")
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
t.Fatalf("body dekodieren: %v", err)
}
w.WriteHeader(http.StatusAccepted)
}))
defer srv.Close()
c := New(srv.URL, "svc-token-123")
err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", map[string]any{"count": 3})
if err != nil {
t.Fatalf("Ingest: %v", err)
}
if gotToken != "svc-token-123" {
t.Fatalf("Service-Token nicht gesendet, erhalten: %q", gotToken)
}
if gotBody.TenantSlug != "acme" {
t.Fatalf("TenantSlug fehlt/falsch im Audit-Ereignis: %q", gotBody.TenantSlug)
}
if gotBody.Action != "connector.sync" {
t.Fatalf("Action falsch: %q", gotBody.Action)
}
}
func TestIngest_UnexpectedStatus_ReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
c := New(srv.URL, "svc-token-123")
if err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", nil); err == nil {
t.Fatal("erwarteter Fehler bei Status 500 blieb aus")
}
}
func TestIngest_TransportError_ReturnsError(t *testing.T) {
c := New("http://127.0.0.1:1", "svc-token-123")
if err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", nil); err == nil {
t.Fatal("erwarteter Transport-Fehler blieb aus")
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
type Recorder struct {
@@ -11,7 +11,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
func setupOutboxTest(t *testing.T) *pgxpool.Pool {
@@ -26,8 +26,8 @@ import (
"strings"
"time"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
)
@@ -13,7 +13,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
connectcrypto "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
@@ -14,8 +14,8 @@ import (
"io"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
)
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"net/http"
"net/url"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
// tenantKEKResponse mirrors Core internal/kek.tenantKEKResponse (JSON
+8 -8
View File
@@ -54,14 +54,14 @@ type triggerRequest struct {
}
type runResponse struct {
ID string `json:"id"`
PeriodStart string `json:"period_start"`
PeriodEnd string `json:"period_end"`
EntryCount int `json:"entry_count"`
RetentionClass string `json:"retention_class"`
CreatedAt string `json:"created_at"`
TransferStatus string `json:"transfer_status,omitempty"`
TransferError string `json:"transfer_error,omitempty"`
ID string `json:"id"`
PeriodStart string `json:"period_start"`
PeriodEnd string `json:"period_end"`
EntryCount int `json:"entry_count"`
RetentionClass string `json:"retention_class"`
CreatedAt string `json:"created_at"`
TransferStatus string `json:"transfer_status,omitempty"`
TransferError string `json:"transfer_error,omitempty"`
}
func (s *Server) handleTrigger(w http.ResponseWriter, r *http.Request, tenantSlug string) {
+7 -7
View File
@@ -34,13 +34,13 @@ var (
// (a deliberate subset, not the full ~116-column spec — DAT-02/DAT-03
// extend format coverage, not this ticket).
type Entry struct {
Amount float64 // Umsatz (ohne Soll/Haben-Kennzeichen), immer positiv
DebitCredit string // "S" (Soll) oder "H" (Haben)
Account string // Konto
CounterAccount string // Gegenkonto (ohne BU-Schlüssel)
Date time.Time // Belegdatum
Text string // Buchungstext
DocumentRef string // Belegfeld 1 (Rechnungs-/Belegnummer)
Amount float64 // Umsatz (ohne Soll/Haben-Kennzeichen), immer positiv
DebitCredit string // "S" (Soll) oder "H" (Haben)
Account string // Konto
CounterAccount string // Gegenkonto (ohne BU-Schlüssel)
Date time.Time // Belegdatum
Text string // Buchungstext
DocumentRef string // Belegfeld 1 (Rechnungs-/Belegnummer)
}
func (e Entry) validate() error {
@@ -89,7 +89,7 @@ func TestExport_StructuralFormatCheck(t *testing.T) {
// AC2: fehlendes Pflichtfeld im Beleg führt zur klaren Abweisung.
func TestExport_MissingMandatoryField_Rejected(t *testing.T) {
cases := []struct {
name string
name string
break_ func(e Entry) Entry
}{
{"Umsatz", func(e Entry) Entry { e.Amount = 0; return e }},
+2 -2
View File
@@ -30,8 +30,8 @@ type Run struct {
}
type Store struct {
Pool *pgxpool.Pool
Archive *archiveclient.Client
Pool *pgxpool.Pool
Archive *archiveclient.Client
// CallbackURL is where Archive would call back on deletion — Connect
// has no export-deletion path yet (DAT-02/03), so this points at a
// stub endpoint that always 501s, same pattern as other modules'
@@ -161,4 +161,3 @@ func countRuns(t *testing.T, pool *pgxpool.Pool, tenantSlug string) int {
}
return n
}
+1 -1
View File
@@ -34,7 +34,7 @@ func TestApplyMapping_UsesGivenMappingNotSuggestion(t *testing.T) {
doc := SourceDocument{
PK: "1",
RawFields: map[string]string{
"title": "Originaltitel",
"title": "Originaltitel",
"custom_title_field": "Korrigierter Titel",
},
}
-1
View File
@@ -241,4 +241,3 @@ func DetectFields(docs []SourceDocument) []string {
}
return order
}
@@ -19,9 +19,9 @@ import (
// assistant in fieldmapping.Store — one mapping per tenant, since a
// tenant realistically migrates from one third-party system at a time.
const (
connectorName = "dms2_import"
connectorInstanceID = "paperless_ngx"
maxUploadBytes int64 = 200 << 20 // 200 MiB: generous for a test/migration export, not unbounded
connectorName = "dms2_import"
connectorInstanceID = "paperless_ngx"
maxUploadBytes int64 = 200 << 20 // 200 MiB: generous for a test/migration export, not unbounded
)
type Server struct {
@@ -55,9 +55,9 @@ func (s *Server) requireIdentity(next func(w http.ResponseWriter, r *http.Reques
}
type previewResponse struct {
DocumentCount int `json:"document_count"`
AvailableFields []string `json:"available_fields"`
SuggestedMapping []fieldmapping.Entry `json:"suggested_mapping"`
DocumentCount int `json:"document_count"`
AvailableFields []string `json:"available_fields"`
SuggestedMapping []fieldmapping.Entry `json:"suggested_mapping"`
Schema []fieldmapping.FieldDef `json:"schema"`
}
@@ -14,8 +14,8 @@ import (
"io"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
)
+10 -10
View File
@@ -49,16 +49,16 @@ type Ref struct {
// Export is one persisted Beleg export attempt.
type Export struct {
ID string
Ref Ref
BelegID string
Status Status
Attempt int
LastError string
RetryAt *time.Time
ERPRef string
CreatedAt time.Time
UpdatedAt time.Time
ID string
Ref Ref
BelegID string
Status Status
Attempt int
LastError string
RetryAt *time.Time
ERPRef string
CreatedAt time.Time
UpdatedAt time.Time
}
var ErrNotFound = errors.New("erpexport: export nicht gefunden")
-25
View File
@@ -1,25 +0,0 @@
// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe
// (Muster aus archive/internal/httpclient, QA-15 F-11): Zeitlimit statt
// endlosem Warten, keine Weiterleitungen (ein Service-Token wuerde sonst an
// das Weiterleitungsziel mitgesendet).
package httpclient
import (
"net/http"
"time"
)
const DefaultTimeout = 15 * time.Second
var shared = New(DefaultTimeout)
func Default() *http.Client { return shared }
func New(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
@@ -1,54 +0,0 @@
package httpclient
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestDefaultHasTimeout(t *testing.T) {
if Default().Timeout != DefaultTimeout || DefaultTimeout <= 0 {
t.Fatalf("timeout = %s", Default().Timeout)
}
}
func TestClientGivesUpOnHangingServer(t *testing.T) {
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-release }))
defer srv.Close()
defer close(release)
start := time.Now()
resp, err := New(200 * time.Millisecond).Get(srv.URL)
if err == nil {
_ = resp.Body.Close()
t.Fatal("kein fehler bei haengendem server")
}
if elapsed := time.Since(start); elapsed > 5*time.Second {
t.Fatalf("aufruf dauerte %s", elapsed)
}
}
func TestClientDoesNotFollowRedirects(t *testing.T) {
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("weiterleitungsziel wurde aufgerufen (Service-Token-Leck)")
}))
defer target.Close()
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
}))
defer redirector.Close()
req, _ := http.NewRequest(http.MethodPost, redirector.URL, strings.NewReader("x"))
req.Header.Set("X-Service-Token", "geheim")
resp, err := Default().Do(req)
if err != nil {
t.Fatalf("do: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusTemporaryRedirect {
t.Fatalf("status = %d, erwartet 307", resp.StatusCode)
}
}
@@ -18,8 +18,8 @@ import (
"strings"
"time"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
)
@@ -1,52 +0,0 @@
// Package policyclient fragt Core (RBAC-06, POST /authorize) nach der
// Rechteentscheidung; Fehler und Ausfall fuehren immer zur Ablehnung.
package policyclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
type Client struct {
BaseURL string
ServiceToken string
HTTP *http.Client
}
func New(baseURL, serviceToken string) *Client {
return &Client{BaseURL: strings.TrimRight(baseURL, "/"), ServiceToken: serviceToken, HTTP: &http.Client{Timeout: 5 * time.Second}}
}
func (c *Client) Allowed(ctx context.Context, tenant, role, permission string) (bool, error) {
body := map[string]string{"role": role, "permission": permission}
if tenant != "" {
body["tenant"] = tenant
}
payload, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/authorize", bytes.NewReader(payload))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Service-Token", c.ServiceToken)
resp, err := c.HTTP.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("policy: status %d", resp.StatusCode)
}
var out struct {
Allowed *bool `json:"allowed"`
}
if err := json.NewDecoder(http.MaxBytesReader(nil, resp.Body, 1<<16)).Decode(&out); err != nil || out.Allowed == nil {
return false, fmt.Errorf("policy: antwort ohne entscheidung")
}
return *out.Allowed, nil
}
@@ -1,80 +0,0 @@
package policyclient
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestAllowed_TenantScopedRequest_TrueOnAllow(t *testing.T) {
var gotBody map[string]string
var gotToken string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotToken = r.Header.Get("X-Service-Token")
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
t.Fatalf("body dekodieren: %v", err)
}
json.NewEncoder(w).Encode(map[string]bool{"allowed": true})
}))
defer srv.Close()
c := New(srv.URL, "svc-token-123")
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
if err != nil {
t.Fatalf("Allowed: %v", err)
}
if !allowed {
t.Fatal("erwartet allowed=true")
}
if gotToken != "svc-token-123" {
t.Fatalf("Service-Token nicht gesendet, erhalten: %q", gotToken)
}
if gotBody["tenant"] != "acme" {
t.Fatalf("Tenant fehlt/falsch in Anfrage: %q", gotBody["tenant"])
}
}
func TestAllowed_DeniedResponse_False(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]bool{"allowed": false})
}))
defer srv.Close()
c := New(srv.URL, "svc-token-123")
allowed, err := c.Allowed(context.Background(), "acme", "viewer", "connector.write")
if err != nil {
t.Fatalf("Allowed: %v", err)
}
if allowed {
t.Fatal("erwartet allowed=false")
}
}
func TestAllowed_ServiceFailure_DeniesInsteadOfSilentAllow(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
c := New(srv.URL, "svc-token-123")
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
if err == nil {
t.Fatal("erwarteter Fehler bei Status 500 blieb aus")
}
if allowed {
t.Fatal("Ausfall darf nie zu allowed=true fuehren")
}
}
func TestAllowed_TransportFailure_DeniesInsteadOfSilentAllow(t *testing.T) {
c := New("http://127.0.0.1:1", "svc-token-123")
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
if err == nil {
t.Fatal("erwarteter Transport-Fehler blieb aus")
}
if allowed {
t.Fatal("Ausfall darf nie zu allowed=true fuehren")
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"net/http"
"time"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/policyclient"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/syncjob"
)
@@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/policyclient"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
)
// newFakePolicyAPI steht fuer Core's echten POST /authorize-Vertrag (nimmt
@@ -9,7 +9,7 @@ import (
"testing"
"time"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/erpconnector"
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/testdb"
+5 -5
View File
@@ -85,11 +85,11 @@ func writeCRM(dir string) error {
contact := map[string]any{
"id": "51023",
"properties": map[string]any{
"firstname": "Anna",
"lastname": "Testperson",
"email": "anna.testperson@musterhandel-nordlicht.example",
"company": "Musterhandel Nordlicht GmbH",
"phone": "+49 431 5550100",
"firstname": "Anna",
"lastname": "Testperson",
"email": "anna.testperson@musterhandel-nordlicht.example",
"company": "Musterhandel Nordlicht GmbH",
"phone": "+49 431 5550100",
"lifecyclestage": "customer",
},
"createdAt": "2025-11-02T09:14:00Z",
@@ -15,7 +15,7 @@ import (
"net/url"
"time"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
type Client struct {
@@ -10,9 +10,7 @@ set -euo pipefail
SECURITY_PACKAGES=(
"internal/crypto"
"internal/credential"
"internal/auditclient"
"internal/audittrail"
"internal/policyclient"
"internal/archiveclient"
"internal/datevexport"
"internal/datevtransfer"