API: mehrere Produkte je Zugang (m2m) + Produktwahl im Order-Request

- api_clients bekommt ein m2m-Feld `products` (Mehrfachauswahl) über die neue
  Junction `api_client_products`; das Einzelfeld `product` bleibt als Fallback.
- POST /v1/orders: neues Pflichtfeld `product` (Produkt-Key) wählt aus den
  erlaubten Produkten; bei genau einem optional. Ungültig/fehlend → 400 mit
  Liste der erlaubten Keys.
- POST /v1/clients akzeptiert `products: [id, …]`.
- Doku (Swagger UI) + OpenAPI um das product-Feld ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lucas Orth
2026-08-18 11:25:46 +02:00
parent 1d5c1a14d4
commit c535af5781
2 changed files with 46 additions and 9 deletions

View File

@@ -453,7 +453,10 @@ const collections = [
f('key_prefix', 'string', { interface: 'input', note: 'Erkennungspräfix des Keys (nur Anzeige).' }),
f('key_hash', 'string', { interface: 'input', note: 'SHA-256 des API-Keys – automatisch gesetzt. Nicht manuell ändern.' }),
m2o('customer', 'directus_users', { note: 'Verknüpftes Kundenkonto (optional).' }),
m2o('product', 'products', { nullable: false, note: 'Einziges Produkt, das dieser Zugang bestellen darf.' }),
// Mehrere erlaubte Produkte (m2m). Die Order gibt per „product" an, welches genutzt wird.
{ field: 'products', type: 'alias', meta: { interface: 'list-m2m', special: ['m2m'], note: 'Erlaubte Produkte für diesen Zugang (Mehrfachauswahl).' }, schema: null },
// Einzelprodukt (optional, Fallback/alt) – wird nur genutzt, wenn keine m2m-Produkte gesetzt sind.
m2o('product', 'products', { note: 'Einzelprodukt (Fallback). Für mehrere Produkte „products" nutzen.' }),
f('active', 'boolean', { interface: 'boolean', default: true }),
f('webhook_url', 'string', { interface: 'input', note: 'Optionaler Webhook für Statusänderungen (derzeit ungenutzt – Kunde pollt).' }),
f('config', 'json', { interface: 'input-code', note: 'Kundenspezifische Konfiguration (optional).' }),
@@ -464,6 +467,12 @@ const collections = [
f('contract_ref', 'string', { interface: 'input', note: 'Referenz zum unterschriebenen Vertrag (Aktenzeichen/Link).' }),
] },
// Junction API-Zugang ↔ Produkt (m2m für api_clients.products).
{ collection: 'api_client_products', meta: { icon: 'link', note: 'Junction API-Zugang ↔ Produkt.', hidden: true }, fields: [
m2o('api_client', 'api_clients', { onDelete: 'CASCADE' }),
m2o('product', 'products', { onDelete: 'CASCADE' }),
] },
// Unterschriften je Kunde (SVG-Datei). Wird am Kundenkonto als o2m gezeigt.
{ collection: 'signatures', meta: { icon: 'draw', note: 'Unterschriften je Kunde.' }, fields: [
m2o('customer', 'directus_users', { onDelete: 'CASCADE' }),
@@ -636,6 +645,9 @@ async function applySchema() {
try {
await api('PATCH', '/relations/order_addons/order', { meta: { one_field: 'addons', junction_field: 'price_item' } });
await api('PATCH', '/relations/order_addons/price_item', { meta: { junction_field: 'order' } });
// m2m api_clients.products ↔ products über api_client_products
await api('PATCH', '/relations/api_client_products/api_client', { meta: { one_field: 'products', junction_field: 'product' } });
await api('PATCH', '/relations/api_client_products/product', { meta: { junction_field: 'api_client' } });
log('m2m verdrahtet');
} catch (e) { console.warn(' m2m-Verdrahtung übersprungen (bitte melden):', e.message); }