mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 23:56:40 +02:00
docs(crm): add frontend integration guide for CRM module
Comprehensive developer handoff document covering all CRM API endpoints, TypeScript interfaces, response formats, React Query hook patterns, and recommended frontend structure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
43877bbb4a
commit
f65b9fb930
1 changed files with 602 additions and 0 deletions
602
docs/CRM_FRONTEND_INTEGRATION.md
Normal file
602
docs/CRM_FRONTEND_INTEGRATION.md
Normal file
|
|
@ -0,0 +1,602 @@
|
|||
# CRM-Modul - Frontend-Integrationsleitfaden
|
||||
|
||||
## Stand: 2026-03-10
|
||||
|
||||
---
|
||||
|
||||
## 1. Uebersicht
|
||||
|
||||
Das CRM-Backend laeuft als eigenstaendiger Container (`insight-crm`) auf Port 3100.
|
||||
Traefik routet alle Anfragen unter `/api/v1/crm/*` automatisch an den CRM-Service.
|
||||
|
||||
**Base-URL:** `/api/v1/crm`
|
||||
**Swagger-Docs:** `http://172.20.10.59/api/v1/crm/docs/`
|
||||
**Authentifizierung:** Gleicher JWT-Token wie Core-Service (Bearer Token im Header)
|
||||
|
||||
Der bestehende Axios-Client (`src/api/client.ts`) funktioniert ohne Aenderung,
|
||||
da er bereits `/api/v1` als Base-URL nutzt und den Authorization-Header automatisch setzt.
|
||||
|
||||
---
|
||||
|
||||
## 2. API-Endpunkte
|
||||
|
||||
### Kontakte
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|-------------|
|
||||
| GET | `/crm/contacts` | Liste (paginiert, suchbar) |
|
||||
| POST | `/crm/contacts` | Kontakt erstellen |
|
||||
| GET | `/crm/contacts/:id` | Kontakt abrufen |
|
||||
| PATCH | `/crm/contacts/:id` | Kontakt aktualisieren |
|
||||
| DELETE | `/crm/contacts/:id` | Kontakt loeschen |
|
||||
|
||||
### Aktivitaeten
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|-------------|
|
||||
| GET | `/crm/activities` | Liste (filterbar nach contactId, type) |
|
||||
| POST | `/crm/activities` | Aktivitaet erstellen |
|
||||
| GET | `/crm/activities/:id` | Aktivitaet abrufen |
|
||||
| PATCH | `/crm/activities/:id` | Aktivitaet aktualisieren |
|
||||
| DELETE | `/crm/activities/:id` | Aktivitaet loeschen |
|
||||
|
||||
### Pipelines
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|-------------|
|
||||
| GET | `/crm/pipelines` | Alle Pipelines (inkl. Stages) |
|
||||
| POST | `/crm/pipelines` | Pipeline erstellen (optional mit Stages) |
|
||||
| GET | `/crm/pipelines/:id` | Pipeline abrufen |
|
||||
| PATCH | `/crm/pipelines/:id` | Pipeline aktualisieren |
|
||||
| DELETE | `/crm/pipelines/:id` | Pipeline loeschen |
|
||||
| POST | `/crm/pipelines/:id/stages` | Stage hinzufuegen |
|
||||
| DELETE | `/crm/pipelines/:id/stages/:stageId` | Stage entfernen |
|
||||
|
||||
### Deals
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|-------------|
|
||||
| GET | `/crm/deals` | Liste (filterbar nach pipeline, stage, status, contact) |
|
||||
| POST | `/crm/deals` | Deal erstellen |
|
||||
| GET | `/crm/deals/:id` | Deal abrufen |
|
||||
| PATCH | `/crm/deals/:id` | Deal aktualisieren |
|
||||
| DELETE | `/crm/deals/:id` | Deal loeschen |
|
||||
|
||||
---
|
||||
|
||||
## 3. Response-Format
|
||||
|
||||
Alle Antworten folgen einem einheitlichen Format:
|
||||
|
||||
### Einzelner Datensatz
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... },
|
||||
"meta": {
|
||||
"timestamp": "2026-03-10T16:42:04.559Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Liste (paginiert)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [ ... ],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pageSize": 25,
|
||||
"total": 42,
|
||||
"totalPages": 2
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-03-10T16:42:04.559Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fehler
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "Validierungsfehler",
|
||||
"details": ["email must be an email"]
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-03-10T16:42:04.559Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Moegliche Error-Codes:** `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `INTERNAL_ERROR`
|
||||
|
||||
---
|
||||
|
||||
## 4. TypeScript-Interfaces
|
||||
|
||||
### Enums
|
||||
|
||||
```typescript
|
||||
type ContactType = 'PERSON' | 'ORGANIZATION';
|
||||
|
||||
type ActivityType = 'NOTE' | 'CALL' | 'EMAIL' | 'MEETING' | 'TASK';
|
||||
|
||||
type DealStatus = 'OPEN' | 'WON' | 'LOST';
|
||||
```
|
||||
|
||||
### Contact
|
||||
|
||||
```typescript
|
||||
interface Contact {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
type: ContactType;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
companyName: string | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
mobile: string | null;
|
||||
website: string | null;
|
||||
street: string | null;
|
||||
zip: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
country: string; // Default: 'DE'
|
||||
notes: string | null;
|
||||
tags: string[];
|
||||
isActive: boolean;
|
||||
createdBy: string;
|
||||
updatedBy: string | null;
|
||||
createdAt: string; // ISO 8601
|
||||
updatedAt: string; // ISO 8601
|
||||
}
|
||||
|
||||
interface CreateContactPayload {
|
||||
type?: ContactType; // Default: 'PERSON'
|
||||
firstName?: string; // max 100
|
||||
lastName?: string; // max 100
|
||||
companyName?: string; // max 200
|
||||
email?: string; // valides E-Mail-Format
|
||||
phone?: string; // max 50
|
||||
mobile?: string; // max 50
|
||||
website?: string; // valide URL
|
||||
street?: string; // max 200
|
||||
zip?: string; // max 20
|
||||
city?: string; // max 100
|
||||
state?: string; // max 100
|
||||
country?: string; // max 2, Default: 'DE'
|
||||
notes?: string;
|
||||
tags?: string[];
|
||||
isActive?: boolean; // Default: true
|
||||
}
|
||||
|
||||
type UpdateContactPayload = Partial<CreateContactPayload>;
|
||||
|
||||
interface ContactQueryParams {
|
||||
page?: number; // Default: 1
|
||||
pageSize?: number; // Default: 25, max: 100
|
||||
search?: string; // Suche in Name, E-Mail, Firma
|
||||
type?: ContactType;
|
||||
sort?: string; // Default: 'createdAt'
|
||||
order?: 'asc' | 'desc'; // Default: 'desc'
|
||||
}
|
||||
```
|
||||
|
||||
### Activity
|
||||
|
||||
```typescript
|
||||
interface Activity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
contactId: string;
|
||||
type: ActivityType;
|
||||
subject: string;
|
||||
description: string | null;
|
||||
scheduledAt: string | null; // ISO 8601
|
||||
completedAt: string | null; // ISO 8601
|
||||
createdBy: string;
|
||||
updatedBy: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
contact?: { // Nur bei Detail-Abfrage
|
||||
id: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
companyName: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateActivityPayload {
|
||||
contactId: string; // Pflicht, UUID
|
||||
type: ActivityType; // Pflicht
|
||||
subject: string; // Pflicht, max 500
|
||||
description?: string;
|
||||
scheduledAt?: string; // ISO 8601
|
||||
completedAt?: string; // ISO 8601
|
||||
}
|
||||
|
||||
// contactId kann NICHT geaendert werden
|
||||
interface UpdateActivityPayload {
|
||||
type?: ActivityType;
|
||||
subject?: string;
|
||||
description?: string;
|
||||
scheduledAt?: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
interface ActivityQueryParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
contactId?: string; // Filter nach Kontakt
|
||||
type?: ActivityType;
|
||||
sort?: string; // Default: 'createdAt'
|
||||
order?: 'asc' | 'desc'; // Default: 'desc'
|
||||
}
|
||||
```
|
||||
|
||||
### Pipeline & Stage
|
||||
|
||||
```typescript
|
||||
interface PipelineStage {
|
||||
id: string;
|
||||
pipelineId: string;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
color: string; // Hex: '#3B82F6'
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Pipeline {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
isActive: boolean;
|
||||
createdBy: string;
|
||||
updatedBy: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
stages: PipelineStage[];
|
||||
}
|
||||
|
||||
interface CreatePipelinePayload {
|
||||
name: string; // Pflicht, max 200
|
||||
isDefault?: boolean; // Default: false
|
||||
stages?: { // Optional: Stages direkt miterstellen
|
||||
name: string; // Pflicht, max 200
|
||||
sortOrder?: number; // Default: 0
|
||||
color?: string; // Hex #RRGGBB, Default: '#6B7280'
|
||||
}[];
|
||||
}
|
||||
|
||||
interface UpdatePipelinePayload {
|
||||
name?: string;
|
||||
isDefault?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// Fuer POST /crm/pipelines/:id/stages
|
||||
interface AddStagePayload {
|
||||
name: string; // Pflicht, max 200
|
||||
sortOrder?: number;
|
||||
color?: string; // Hex #RRGGBB
|
||||
}
|
||||
```
|
||||
|
||||
### Deal
|
||||
|
||||
```typescript
|
||||
interface Deal {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
pipelineId: string;
|
||||
stageId: string;
|
||||
contactId: string | null;
|
||||
title: string;
|
||||
value: string | null; // Decimal als String (z.B. "24000")
|
||||
currency: string; // Default: 'EUR'
|
||||
status: DealStatus;
|
||||
expectedCloseDate: string | null;
|
||||
closedAt: string | null; // Wird automatisch gesetzt bei WON/LOST
|
||||
notes: string | null;
|
||||
createdBy: string;
|
||||
updatedBy: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
pipeline?: { // Nur bei Detail/Liste
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
stage?: {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
contact?: {
|
||||
id: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
companyName: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface CreateDealPayload {
|
||||
pipelineId: string; // Pflicht, UUID
|
||||
stageId: string; // Pflicht, UUID
|
||||
title: string; // Pflicht, max 500
|
||||
contactId?: string; // Optional, UUID
|
||||
value?: number; // Decimal, min 0, max 2 Nachkommastellen
|
||||
currency?: string; // max 3, Default: 'EUR'
|
||||
status?: DealStatus; // Default: 'OPEN'
|
||||
expectedCloseDate?: string; // ISO 8601
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
type UpdateDealPayload = Partial<CreateDealPayload>;
|
||||
|
||||
interface DealQueryParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
pipelineId?: string; // Filter nach Pipeline
|
||||
stageId?: string; // Filter nach Stage
|
||||
contactId?: string; // Filter nach Kontakt
|
||||
status?: DealStatus; // Filter nach Status
|
||||
search?: string; // Suche im Titel
|
||||
sort?: string; // Default: 'createdAt'
|
||||
order?: 'asc' | 'desc'; // Default: 'desc'
|
||||
}
|
||||
```
|
||||
|
||||
### API-Response Wrapper
|
||||
|
||||
```typescript
|
||||
interface CrmPaginatedResponse<T> {
|
||||
success: true;
|
||||
data: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
meta: { timestamp: string };
|
||||
}
|
||||
|
||||
interface CrmSingleResponse<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
meta: { timestamp: string };
|
||||
}
|
||||
|
||||
interface CrmErrorResponse {
|
||||
success: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: string[];
|
||||
};
|
||||
meta: { timestamp: string };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API-Client Beispiele
|
||||
|
||||
Der vorhandene Axios-Client (`src/api/client.ts`) kann direkt verwendet werden:
|
||||
|
||||
```typescript
|
||||
import api from '../api/client';
|
||||
|
||||
// --- Contacts ---
|
||||
|
||||
// Liste abrufen (mit Suche und Pagination)
|
||||
const { data } = await api.get<CrmPaginatedResponse<Contact>>('/crm/contacts', {
|
||||
params: { search: 'Mustermann', page: 1, pageSize: 25 }
|
||||
});
|
||||
|
||||
// Kontakt erstellen
|
||||
const { data } = await api.post<CrmSingleResponse<Contact>>('/crm/contacts', {
|
||||
type: 'PERSON',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
email: 'max@example.com',
|
||||
companyName: 'Xinion GmbH',
|
||||
});
|
||||
|
||||
// Kontakt aktualisieren
|
||||
const { data } = await api.patch<CrmSingleResponse<Contact>>(`/crm/contacts/${id}`, {
|
||||
city: 'Berlin',
|
||||
tags: ['VIP', 'Partner'],
|
||||
});
|
||||
|
||||
// Kontakt loeschen
|
||||
await api.delete(`/crm/contacts/${id}`);
|
||||
|
||||
|
||||
// --- Deals ---
|
||||
|
||||
// Deal-Liste mit Pipeline-Filter
|
||||
const { data } = await api.get<CrmPaginatedResponse<Deal>>('/crm/deals', {
|
||||
params: { pipelineId: '...', status: 'OPEN' }
|
||||
});
|
||||
|
||||
// Deal erstellen
|
||||
const { data } = await api.post<CrmSingleResponse<Deal>>('/crm/deals', {
|
||||
title: 'Enterprise Lizenz',
|
||||
pipelineId: '...',
|
||||
stageId: '...',
|
||||
contactId: '...',
|
||||
value: 50000,
|
||||
currency: 'EUR',
|
||||
expectedCloseDate: '2026-06-30T00:00:00.000Z',
|
||||
});
|
||||
|
||||
// Deal-Stage verschieben (z.B. Drag & Drop im Kanban)
|
||||
await api.patch(`/crm/deals/${id}`, { stageId: newStageId });
|
||||
|
||||
// Deal als gewonnen markieren (closedAt wird automatisch gesetzt)
|
||||
await api.patch(`/crm/deals/${id}`, { status: 'WON' });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. React-Query Hooks (Vorschlag)
|
||||
|
||||
```typescript
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '../api/client';
|
||||
|
||||
// Query-Keys
|
||||
const crmKeys = {
|
||||
contacts: ['crm', 'contacts'] as const,
|
||||
contact: (id: string) => ['crm', 'contacts', id] as const,
|
||||
activities: ['crm', 'activities'] as const,
|
||||
pipelines: ['crm', 'pipelines'] as const,
|
||||
deals: ['crm', 'deals'] as const,
|
||||
deal: (id: string) => ['crm', 'deals', id] as const,
|
||||
};
|
||||
|
||||
// Kontakte abrufen
|
||||
function useContacts(params?: ContactQueryParams) {
|
||||
return useQuery({
|
||||
queryKey: [...crmKeys.contacts, params],
|
||||
queryFn: () => api.get('/crm/contacts', { params }).then(r => r.data),
|
||||
});
|
||||
}
|
||||
|
||||
// Kontakt erstellen
|
||||
function useCreateContact() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (payload: CreateContactPayload) =>
|
||||
api.post('/crm/contacts', payload).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: crmKeys.contacts }),
|
||||
});
|
||||
}
|
||||
|
||||
// Pipelines (selten geaendert, laenger cachen)
|
||||
function usePipelines() {
|
||||
return useQuery({
|
||||
queryKey: crmKeys.pipelines,
|
||||
queryFn: () => api.get('/crm/pipelines').then(r => r.data),
|
||||
staleTime: 10 * 60 * 1000, // 10 Minuten
|
||||
});
|
||||
}
|
||||
|
||||
// Deals mit Filtern
|
||||
function useDeals(params?: DealQueryParams) {
|
||||
return useQuery({
|
||||
queryKey: [...crmKeys.deals, params],
|
||||
queryFn: () => api.get('/crm/deals', { params }).then(r => r.data),
|
||||
});
|
||||
}
|
||||
|
||||
// Deal-Stage verschieben (Kanban Drag & Drop)
|
||||
function useMoveDeal() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, stageId }: { id: string; stageId: string }) =>
|
||||
api.patch(`/crm/deals/${id}`, { stageId }).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: crmKeys.deals }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontend-Struktur (Vorschlag)
|
||||
|
||||
```
|
||||
src/
|
||||
crm/ # CRM-Modul
|
||||
types.ts # Alle CRM TypeScript-Interfaces
|
||||
api.ts # API-Aufrufe (axios calls)
|
||||
hooks.ts # React-Query Hooks
|
||||
CrmLayout.tsx # Layout mit Sidebar/Tabs
|
||||
CrmLayout.module.css
|
||||
contacts/
|
||||
ContactsPage.tsx # Kontaktliste mit Suche + Filter
|
||||
ContactDetailPage.tsx # Kontaktdetail mit Aktivitaeten
|
||||
ContactForm.tsx # Erstellen/Bearbeiten-Formular
|
||||
deals/
|
||||
DealsPage.tsx # Kanban-Board oder Listenansicht
|
||||
DealDetailPage.tsx # Deal-Detail
|
||||
DealForm.tsx # Erstellen/Bearbeiten-Formular
|
||||
KanbanBoard.tsx # Drag & Drop Pipeline-Ansicht
|
||||
KanbanBoard.module.css
|
||||
pipelines/
|
||||
PipelineSettings.tsx # Pipeline-Konfiguration (Admin)
|
||||
activities/
|
||||
ActivityTimeline.tsx # Aktivitaeten-Timeline (in Kontaktdetail)
|
||||
ActivityForm.tsx # Neue Aktivitaet erstellen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Routing-Integration
|
||||
|
||||
In `src/shell/App.tsx` neue Routen hinzufuegen:
|
||||
|
||||
```tsx
|
||||
// Innerhalb des geschuetzten PrivateRoute-Bereichs
|
||||
<Route path="/crm" element={<CrmLayout />}>
|
||||
<Route index element={<Navigate to="contacts" replace />} />
|
||||
<Route path="contacts" element={<ContactsPage />} />
|
||||
<Route path="contacts/:id" element={<ContactDetailPage />} />
|
||||
<Route path="deals" element={<DealsPage />} />
|
||||
<Route path="deals/:id" element={<DealDetailPage />} />
|
||||
<Route path="pipelines" element={<PipelineSettings />} />
|
||||
</Route>
|
||||
```
|
||||
|
||||
Sidebar-Navigation: Neuen Menuepunkt "CRM" mit Untereintraegen (Kontakte, Deals, Pipelines).
|
||||
|
||||
---
|
||||
|
||||
## 9. Wichtige Hinweise
|
||||
|
||||
### Validierung
|
||||
- Das Backend validiert streng: Unbekannte Felder werden abgelehnt (`forbidNonWhitelisted`)
|
||||
- Nur die dokumentierten Felder im Request-Body senden
|
||||
- UUIDs muessen valides UUID-v4-Format haben
|
||||
|
||||
### Deal-Werte
|
||||
- `value` wird als Decimal gespeichert, kommt aber als **String** zurueck (z.B. `"24000"`)
|
||||
- Beim Anzeigen `parseFloat()` oder `Number()` verwenden
|
||||
- Beim Senden als Number senden (z.B. `value: 24000`)
|
||||
|
||||
### Automatische Felder
|
||||
- `closedAt` bei Deals wird automatisch gesetzt wenn `status` auf `WON` oder `LOST` geaendert wird
|
||||
- `createdBy` / `updatedBy` werden automatisch aus dem JWT-Token befuellt
|
||||
- `tenantId` wird automatisch aus dem JWT-Token gefiltert (Multi-Tenancy)
|
||||
|
||||
### Loeschverhalten (Cascade)
|
||||
- Kontakt loeschen -> alle Aktivitaeten dieses Kontakts werden mitgeloescht
|
||||
- Kontakt loeschen -> `contactId` bei Deals wird auf `null` gesetzt (Deal bleibt)
|
||||
- Pipeline loeschen -> alle Stages und Deals dieser Pipeline werden mitgeloescht
|
||||
|
||||
### Suche
|
||||
- Kontakte: Sucht in `firstName`, `lastName`, `companyName`, `email`
|
||||
- Deals: Sucht in `title`
|
||||
- Keine Volltextsuche, nur LIKE-basiert (Teilstring-Match)
|
||||
|
||||
---
|
||||
|
||||
## 10. Testdaten & Swagger
|
||||
|
||||
**Swagger UI:** http://172.20.10.59/api/v1/crm/docs/
|
||||
|
||||
Dort koennen alle Endpunkte interaktiv getestet werden.
|
||||
Oben rechts "Authorize" klicken und einen gueltigen JWT-Token eingeben.
|
||||
|
||||
Ein Token kann ueber die normale Plattform-Anmeldung generiert werden
|
||||
(Login -> Token aus Browser-DevTools/Network-Tab kopieren).
|
||||
Loading…
Add table
Reference in a new issue