# Admin-Recovery — User manuell zu Admin / Superadmin machen (Docker)

Praktische Anleitung fuer den Fall, dass eine eForms-Instanz keinen
funktionierenden Admin oder Superadmin mehr hat — z. B. nach einem
versehentlichen DB-Reset, einem fehlgeschlagenen Restore oder einem
verlorenen Passwort.

Alle Befehle laufen auf dem Docker-Host im Compose-Verzeichnis (typisch
`/opt/eforms-cloud/`). Der Laravel-Container ist der Service **`api`**.

> **Wichtig:** Die Rolle `superadmin` ist die einzige Rolle, die Zugriff
> auf das ePlatform-Modul gewaehrt. Sie ist **ausschliesslich** per
> Artisan-Console vergebbar — UI und API verweigern jede Zuweisung mit
> 403. Das ist Design, kein Bug.

---

## Rollen-Modell

| Rolle | Berechtigung |
|---|---|
| `user` | Meldungen einsehen, Dashboard |
| `editor` | + Block-/Template-Editor, Import/Export |
| `admin` | + Userverwaltung, Settings, Tokens, SSO, Reports, Audit |
| `superadmin` | + ePlatform-Modul (Tenant-Management) — **nur per Konsole** |

Die `superadmin`-Rolle wird zusaetzlich an Mitgliedschaft in der
Platform-Owner-Org gebunden (die Org, deren `domain` gleich
`APP_BASE_DOMAIN` ist). Frontend AND-koppelt `role === 'superadmin'`
mit `is_platform_owner` — beides muss stimmen, sonst bleibt die
ePlatform-Sidebar unsichtbar.

---

## Voraussetzungen pruefen (immer zuerst)

### `APP_BASE_DOMAIN` muss gesetzt sein

```bash
docker compose exec api php artisan tinker --execute="echo 'base_domain='.config('app.base_domain').PHP_EOL;"
```

Erwartet: dein Hauptdomain-String (z. B. `eforms.cloud`). Wenn leer:
in der `.env` im Compose-Verzeichnis setzen, dann Config-Cache leeren:

```bash
docker compose exec api php artisan config:clear
docker compose restart api
```

### Platform-Owner-Org muss existieren

```bash
docker compose exec api php artisan tinker --execute="\$d=config('app.base_domain'); \$o=App\Models\Organization::where('domain',\$d)->where('is_active',true)->first(); echo \$o ? 'OwnerOrg id='.\$o->id.' name='.\$o->name.PHP_EOL : 'KEINE OWNER-ORG'.PHP_EOL;"
```

Erwartet: `OwnerOrg id=… name=…`. Wenn keine existiert: `eforms:install-admin`
(siehe unten) legt sie automatisch an. Wenn die erste Org eine falsche
`domain` hat, korrigieren:

```bash
docker compose exec api php artisan tinker --execute="\$o=App\Models\Organization::first(); if(!\$o){echo 'KEINE ORG'.PHP_EOL;exit;} \$o->domain='eforms.cloud'; \$o->is_active=true; \$o->save(); echo 'ok id='.\$o->id.PHP_EOL;"
```

> **Quoting-Hinweis:** Domain-String direkt einsetzen, nicht
> `config('eforms.cloud')` — das ist ein Config-Key-Lookup und liefert
> `null`. `config('app.base_domain')` liest die Env-Var.

### User auflisten

```bash
docker compose exec api php artisan tinker --execute="App\Models\User::select('id','email','role','organization_id','role_id')->orderBy('id')->get()->each(fn(\$u)=>print(\$u->id.' | '.\$u->email.' | role='.\$u->role.' | org='.\$u->organization_id.' | role_id='.\$u->role_id.PHP_EOL));"
```

Pruefe, ob ueberhaupt noch ein Superadmin existiert:

```bash
docker compose exec api php artisan tinker --execute="echo 'superadmins='.App\Models\User::where('role','superadmin')->count().PHP_EOL;"
```

---

## Weg A — `eforms:install-admin` (empfohlen)

Idempotent, bootstrappt bei Bedarf auch Default-Org, Module-Subscriptions,
managed Tenant-Domain und System-Rollen. Funktioniert auch bei bereits
existierendem User (Tenant-Bootstrap laeuft trotzdem).

### Schritt 1 — Admin anlegen

Email **zwingend lowercase**:

```bash
docker compose exec api php artisan eforms:install-admin --email=admin@example.tld --name="Admin"
```

Das generierte Passwort wird **einmalig** im Klartext ausgegeben — sofort
sichern. Mit eigenem Passwort:

```bash
docker compose exec api php artisan eforms:install-admin --email=admin@example.tld --name="Admin" --password='DeinPasswort2026'
```

> **Bash-Quoting:** Immer einfache Anfuehrungszeichen `'…'` ums Passwort.
> Doppelte Quotes lassen Bash `$` expandieren, `!` triggert History-
> Expansion (z. B. `!word` wird zu altem Command-Snippet). Wenn das
> Passwort `!` oder `$` enthaelt, vorher `set +H` ausfuehren oder
> Sonderzeichen vermeiden.

### Schritt 2 — Zum Superadmin befoerdern

Vergibt `role=superadmin`, verschiebt den User in die Platform-Owner-Org
und setzt `role_id` auf die dortige superadmin-System-Rolle:

```bash
docker compose exec api php artisan user:promote-superadmin admin@example.tld --force
```

### Schritt 3 — Verifizieren

```bash
docker compose exec api php artisan tinker --execute="\$u=App\Models\User::where('email','admin@example.tld')->first(); \$owner=App\Models\Organization::where('domain',config('app.base_domain'))->value('id'); echo 'role='.\$u->role.' org='.\$u->organization_id.' (owner='.\$owner.') role_id='.\$u->role_id.' platform_owner='.((\$u->role==='superadmin' && \$u->organization_id===\$owner)?'true':'FALSE').PHP_EOL;"
```

Erwartet:

```
role=superadmin org=1 (owner=1) role_id=1 platform_owner=true
```

`org` muss gleich `owner` sein, `role_id` darf nicht leer sein,
`platform_owner=true` ist Pflicht — sonst bleibt die ePlatform-Sidebar
unsichtbar.

---

## Weg B — Passwort eines bestehenden Users zuruecksetzen

Wenn der Account schon existiert und korrekt eingerichtet ist, nur das
Passwort verloren wurde.

### Variante B1 — via Eloquent (nutzt `hashed`-Cast)

```bash
docker compose exec api php artisan tinker --execute="\$u=App\Models\User::where('email','admin@example.tld')->first(); \$u->password='NeuesPasswort2026'; \$u->save(); echo 'check='.(Hash::check('NeuesPasswort2026',\$u->fresh()->password)?'OK':'FAIL').PHP_EOL;"
```

Das `hashed`-Cast im User-Model haseht das Passwort genau einmal. Die
`Hash::check`-Verifikation am Ende ist Pflicht: wenn `OK`, ist der
Login garantiert moeglich; wenn `FAIL`, war was an der Eingabe falsch
(Bash-Quoting, Tippfehler).

### Variante B2 — Query-Builder (umgeht Cast, garantiert ein Hash)

Fuer den Notfall, falls je ein Double-Hash-Verdacht aufkommt:

```bash
docker compose exec api php artisan tinker --execute="\$hash=Hash::make('NeuesPasswort2026'); \$n=App\Models\User::where('email','admin@example.tld')->update(['password'=>\$hash]); echo 'updated='.\$n.' check='.(Hash::check('NeuesPasswort2026',App\Models\User::where('email','admin@example.tld')->value('password'))?'OK':'FAIL').PHP_EOL;"
```

`update(['password'=>$hash])` per Query-Builder triggert das `hashed`-Cast
nicht — `Hash::make()` haseht einmal explizit, in der DB landet genau
das Ergebnis.

### Hash-Pruefung als Diagnose

Wenn ein Login partout nicht klappt: pruefe Hash-Format und teste direkt
gegen das Klartext-Passwort, das du gesetzt zu haben glaubst:

```bash
docker compose exec api php artisan tinker --execute="\$u=App\Models\User::where('email','admin@example.tld')->first(); echo 'hash_prefix='.substr(\$u->password,0,7).' len='.strlen(\$u->password).' check='.(Hash::check('NeuesPasswort2026',\$u->password)?'OK':'FAIL').PHP_EOL;"
```

Ein gesunder bcrypt-Hash hat das Format `$2y$12$…` und Laenge **60**.

- `prefix=$2y$12$ len=60 check=OK` → Hash OK, Login muss klappen. Wenn
  trotzdem „ungueltige Anmeldedaten": Browser-Autofill, Caps-Lock,
  Trailing-Whitespace beim Copy-Paste oder die Eingabe-Email hat
  Mixed-Case (Server normalisiert, aber pruefen schadet nicht).
- `len != 60` oder Prefix anders → Hash kaputt, Variante B2 nutzen.

---

## Weg C — Bestehenden User zum Admin (nicht Superadmin) befoerdern

Falls nur normaler Admin-Zugriff noetig ist (kein ePlatform-Modul):
geht entweder ueber die UI (jeder Admin kann das) oder per Tinker mit
korrektem `role_id` auf die Administrator-System-Rolle der Org:

```bash
docker compose exec api php artisan tinker --execute="\$email='user@example.tld'; \$u=App\Models\User::where('email',\$email)->first(); if(!\$u){echo 'kein user'.PHP_EOL;exit;} \$r=App\Models\Role::where('organization_id',\$u->organization_id)->where('base_role','admin')->where('is_system',true)->first(); if(!\$r){echo 'admin-rolle fehlt — RoleSeeder::seedForOrg(\$u->organization) ausfuehren'.PHP_EOL;exit;} \$u->role_id=\$r->id; \$u->save(); echo 'role='.\$u->fresh()->role.' role_id='.\$u->role_id.PHP_EOL;"
```

`role` (String) wird automatisch vom `saving`-Hook im User-Model aus
`role_id` synchronisiert (`User::booted()`).

Wenn die Admin-System-Rolle fuer die Org fehlt:

```bash
docker compose exec api php artisan tinker --execute="\$o=App\Models\Organization::find(1); Database\Seeders\RoleSeeder::seedForOrg(\$o); echo 'ok'.PHP_EOL;"
```

---

## Weg D — Superadmin demoten

Spiegelbild zu `user:promote-superadmin`:

```bash
docker compose exec api php artisan user:demote-superadmin admin@example.tld --force
```

Der User behaelt seine Mitgliedschaft in der Platform-Owner-Org, faellt
aber auf `admin` zurueck. Sinnvoll z. B. vor dem Loeschen eines
Plattform-Accounts oder beim Uebergeben an einen anderen Operator.

---

## Stolperfallen (historische Bugs — heute alle gefixt)

Die folgenden Probleme sind in HEAD geloest. Wenn man bei einer alten
Version landet, koennen sie wieder auftauchen:

| Symptom | Ursache | Fix-Commit |
|---|---|---|
| Login schlaegt fehl trotz richtigem Passwort | CR/LF aus Env in Passwort-Hash | `2c027bb` |
| `/admin`-Endpoints 403 oder leer trotz `role='admin'` | `role_id` (FK) fehlt — RoleSeeder lief nie | `5a54c54` |
| `/admin/domains` ohne Anker | keine managed Zeile in `tenant_domains` | `0e0ecf0` |
| Login findet User nicht | Mixed-Case Email in DB, Postgres case-sensitive | `04dd260` |
| ePlatform-Sidebar leer trotz `role=superadmin` | superadmin-Rolle in falscher Org (nicht Platform-Owner) | `04dd260` |
| Bestehender User → Tenant-Bootstrap uebersprungen | falsche Reihenfolge im Install-Command | `6a39d39` |

---

## Regeln (sticky)

1. **Email immer lowercase** beim Anlegen und beim Login.
2. **`APP_BASE_DOMAIN` muss vor Bootstrap gesetzt sein** — sonst hat die
   Default-Org keine `domain`, und `user:promote-superadmin` bricht
   ab mit „Platform-Owner-Org konnte nicht aufgeloest werden".
3. **`superadmin` niemals per SQL oder API setzen** — nur ueber
   `user:promote-superadmin`. Ein per SQL gesetzter `superadmin` ohne
   Mitgliedschaft in der Platform-Owner-Org loest die Cleanup-Migration
   beim naechsten Deploy zurueck auf `administrator`.
4. **Passwoerter immer in einfachen Quotes** (`'…'`), nie in doppelten.
   Bei `!`/`$` im Passwort vorher `set +H`.
5. **Nach jedem Passwort-Reset mit `Hash::check()` verifizieren** —
   sonst weiss man im Fehlerfall nicht, ob das Problem an Hash, Eingabe
   oder Browser-Autofill liegt.
6. **Nach erfolgreichem Login sofort 2FA aktivieren** — gerade
   Superadmin-Accounts haben Plattform-weiten Zugriff.

---

## Quick-Reference (Copy-Paste-Sequenz)

Vollstaendiger Recovery-Lauf:

```bash
# 0. Base-Domain pruefen
docker compose exec api php artisan tinker --execute="echo config('app.base_domain').PHP_EOL;"

# 1. Admin anlegen + Tenant bootstrappen
docker compose exec api php artisan eforms:install-admin \
  --email=admin@example.tld \
  --name="Admin" \
  --password='InitialPasswort2026'

# 2. Zum Superadmin befoerdern
docker compose exec api php artisan user:promote-superadmin admin@example.tld --force

# 3. Verifizieren (Rolle + Hash)
docker compose exec api php artisan tinker --execute="\$u=App\Models\User::where('email','admin@example.tld')->first(); \$owner=App\Models\Organization::where('domain',config('app.base_domain'))->value('id'); echo 'role='.\$u->role.' org='.\$u->organization_id.'==owner='.\$owner.' role_id='.\$u->role_id.' check='.(Hash::check('InitialPasswort2026',\$u->password)?'OK':'FAIL').PHP_EOL;"
```

Erwartete Ausgabe von Schritt 3:

```
role=superadmin org=1==owner=1 role_id=1 check=OK
```

Danach Login auf `https://<deine-base-domain>/login` — die ePlatform-
Sidebar muss sichtbar sein.
