# Multi-Tenancy

Multi-Tenancy ermoeglicht den Betrieb mehrerer Organisationen auf einer eforms.cloud-Instanz. Jede Organisation sieht nur ihre eigenen Daten. Verfuegbar ab dem Enterprise-Plan.

## Organization-Model

Die `organizations`-Tabelle speichert Tenant-Daten:

| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | integer | Primary Key |
| `name` | string | Anzeigename der Organisation |
| `slug` | string | Subdomain-Bezeichner (unique) |
| `domain` | string? | Optionale Custom Domain (unique) |
| `is_active` | boolean | Organisation aktiv/deaktiviert |
| `settings` | JSONB? | Organisationsspezifische Einstellungen |

Beziehung: `Organization hasMany Users` ueber `users.organization_id`.

## TenantContext Service

Der `TenantContext` ist ein Request-Scoped Singleton, der die aktuelle Organisation fuer den laufenden Request speichert.

```php
// Schreiben (durch Middleware)
$tenantContext->set($organization);

// Lesen (in Services, Models, Controllers)
$tenantContext->get();    // Organization|null
$tenantContext->id();     // int|null
$tenantContext->has();    // bool
$tenantContext->clear();  // Reset
```

Der TenantContext wird in der Laravel Service Container als Singleton registriert und lebt fuer die Dauer eines Requests.

## BelongsToTenant Trait

Models die den `BelongsToTenant`-Trait verwenden, werden automatisch nach Tenant gefiltert.

### Zwei Mechanismen

**1. Global Scope (Lesen):**

Jede Query auf ein Tenant-Model wird automatisch um `WHERE organization_id = :current_tenant` erweitert. Wenn kein Tenant gesetzt ist, wird kein Filter angewendet.

```php
// Automatisch: SELECT * FROM submissions WHERE organization_id = 5
Submission::all();
```

**2. Auto-Set (Schreiben):**

Beim Erstellen eines neuen Records wird `organization_id` automatisch auf den aktuellen Tenant gesetzt, sofern nicht explizit angegeben.

```php
// organization_id wird automatisch gesetzt
Submission::create(['template_slug' => 'schnell', 'data' => [...]]);
```

### Relationship

Der Trait fuegt automatisch eine `organization()`-Beziehung hinzu:

```php
$submission->organization; // BelongsTo Organization
```

## EnsureTenant Middleware

Die `EnsureTenant`-Middleware resolved den aktuellen Tenant bei jedem Request. Die Aufloesung erfolgt in folgender Reihenfolge:

```mermaid
graph TD
    R["Request eingehend"] --> D{"Custom Domain\nmatcht organizations.domain?"}
    D -->|Ja| Set["TenantContext.set(org)"]
    D -->|Nein| S{"Subdomain\nmatcht organizations.slug?"}
    S -->|Ja| Set
    S -->|Nein| U{"Auth User\nhat organization_id?"}
    U -->|Ja| Set
    U -->|Nein| None["Kein Tenant\n(Single-Tenant-Modus)"]
    Set --> Active{"org.is_active?"}
    Active -->|Ja| Next["Request fortsetzen"]
    Active -->|Nein| Block["403 - Organisation deaktiviert"]
```

### Domain-Aufloesung

1. **Custom Domain:** `request.host` wird gegen `organizations.domain` geprueft.
2. **Subdomain:** Host wird gegen `APP_BASE_DOMAIN` aufgespalten. `acme.eforms.cloud` → slug `acme`.
3. **User-Fallback:** Der authentifizierte User's `organization_id` wird verwendet.

Wenn keine Organisation gefunden wird, laeuft der Request ohne Tenant-Kontext (Single-Tenant-Modus).

### Deaktivierte Organisationen

Wenn eine Organisation `is_active: false` hat, gibt die Middleware `403` zurueck:

```json
{ "message": "Diese Organisation ist deaktiviert." }
```

## Einladungs-Flow

```mermaid
sequenceDiagram
    participant A as Admin
    participant API as API
    participant E as E-Mail
    participant U as Eingeladener

    A->>API: POST /admin/organization/invite {email, role}
    API->>API: Pruefen: Bereits Mitglied? Offene Einladung?
    API->>API: OrganizationInvitation erstellen (Token, 7 Tage gueltig)
    API->>E: InvitationMail senden
    E->>U: Link: /einladung/{token}
    U->>API: GET /invitations/{token}
    API-->>U: Einladungsdetails (Organisation, Rolle)
    U->>API: POST /invitations/{token}/accept
    API->>API: User.organization_id setzen, Einladung als akzeptiert markieren
```

### Einladungs-Regeln

- Maximale Gueltigkeit: 7 Tage
- Keine doppelten Einladungen an dieselbe E-Mail
- Bereits vorhandene User werden der Organisation zugeordnet
- Neue User muessen ein Konto erstellen
- Rollen bei Einladung: `user`, `editor`, `admin` (nicht `developer`)

## Mitglieder-Verwaltung

### Mitglieder auflisten

`GET /api/admin/organization/members`

```json
[
  { "id": 1, "name": "Max Mustermann", "email": "max@example.com", "role": "admin", "created_at": "..." }
]
```

### Mitglied entfernen

`DELETE /api/admin/organization/members/{userId}`

- Setzt `user.organization_id` auf `null`
- Eigenes Konto kann nicht entfernt werden
- User wird nicht geloescht, nur aus der Organisation entfernt

### Offene Einladungen

`GET /api/admin/organization/invitations` - Zeigt alle noch nicht akzeptierten, nicht abgelaufenen Einladungen.

### Organisation aktualisieren

`PUT /api/admin/organization` - Name, Slug, Custom Domain und Settings aenderbar.

```json
{
  "name": "Klinikum Musterstadt",
  "slug": "klinikum-musterstadt",
  "domain": "meldungen.klinikum-musterstadt.de",
  "settings": {}
}
```

## Alle Organisationen (Super-Admin)

`GET /api/admin/organizations` - Nur fuer Admins sichtbar, zeigt alle Organisationen mit User-Count. Erfordert `multi_tenancy`-Plan-Feature.
