vcard4reseller/backend/src/Controller/ResellerProvisioningController.php
Thomas Peterson bcc06e697b Rechte: User in Employee verschmolzen (eine Identität pro Person)
Beseitigt die Doppelung Admin-Login vs. Mitarbeiter — jeder ist ein Employee
mit optionalem Login/Rechtegruppe (Voraussetzung für Mitarbeiter-Zeiterfassung).

- Employee implementiert UserInterface/PasswordAuthenticated (loginEmail unique,
  password, roles); User-Entität entfernt; Security-Provider → Employee.loginEmail
- Plattform = Reseller mit isPlatform + Org-Firma; Reseller haben Org-Firma
  (Company.isResellerOrg) für ihr Personal → alles = Reseller→Firma→Mitarbeiter
- TenantContext leitet Reseller/Company aus dem Mitarbeiter ab (Reseller-/
  Plattform-Admin = reseller-weit)
- UserAdminController: Login pro Mitarbeiter vergeben/entziehen
  (POST/DELETE /api/employees/{id}/login), /api/users = Logins-Übersicht
- Provisioning/Seed auf das neue Modell; Migrationen zu einer Baseline gesquasht
- Frontend: EmployeesView Login-Block + UsersView (Logins-Übersicht)

Verifiziert: Login, /me, Mandantenscoping, delegierter Grant (Eskalation→403),
öffentliches Profil, SPA-Flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:27:38 +02:00

94 lines
3.3 KiB
PHP

<?php
namespace App\Controller;
use App\Entity\Company;
use App\Entity\Employee;
use App\Entity\PlatformPlan;
use App\Entity\Reseller;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Legt einen Reseller an: Reseller + Org-Firma (für sein Personal) + optional
* einen Admin-Mitarbeiter mit Login. Nur für Plattform-Admins.
*/
#[IsGranted('ROLE_PLATFORM_ADMIN')]
final class ResellerProvisioningController
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly UserPasswordHasherInterface $hasher,
) {
}
#[Route('/api/platform/provision-reseller', name: 'platform_provision_reseller', methods: ['POST'])]
public function __invoke(Request $request): JsonResponse
{
$d = json_decode($request->getContent(), true) ?? [];
$name = trim((string) ($d['name'] ?? ''));
$slug = trim((string) ($d['slug'] ?? ''));
if ('' === $name || '' === $slug) {
return new JsonResponse(['error' => 'name und slug sind erforderlich.'], 422);
}
$reseller = (new Reseller())->setName($name)->setSlug($slug);
if (!empty($d['primaryDomain'])) {
$reseller->setPrimaryDomain((string) $d['primaryDomain']);
}
if (!empty($d['planId'])) {
$plan = $this->em->getRepository(PlatformPlan::class)->find($d['planId']);
if ($plan instanceof PlatformPlan) {
$reseller->setPlatformPlan($plan);
}
}
// Org-Firma des Resellers (beherbergt dessen Personal)
$orgCompany = (new Company())
->setName($name)
->setSlug($slug.'-team')
->setReseller($reseller)
->setIsResellerOrg(true);
$this->em->persist($reseller);
$this->em->persist($orgCompany);
$adminEmail = trim((string) ($d['adminEmail'] ?? ''));
$adminPassword = (string) ($d['adminPassword'] ?? '');
$admin = null;
if ('' !== $adminEmail && '' !== $adminPassword) {
$admin = (new Employee())
->setFirstName((string) ($d['adminFirstName'] ?? 'Admin'))
->setLastName((string) ($d['adminLastName'] ?? $name))
->setSlug('admin')
->setCompany($orgCompany)
->setLoginEmail($adminEmail)
->setRoles([Employee::ROLE_RESELLER_ADMIN]);
$admin->setEmail($adminEmail);
$admin->setPassword($this->hasher->hashPassword($admin, $adminPassword));
}
try {
if ($admin) {
$this->em->persist($admin);
}
$this->em->flush();
} catch (UniqueConstraintViolationException) {
return new JsonResponse(['error' => 'Slug oder Admin-E-Mail bereits vergeben.'], 422);
}
return new JsonResponse([
'id' => (string) $reseller->getId(),
'name' => $reseller->getName(),
'slug' => $reseller->getSlug(),
'adminCreated' => null !== $admin,
], 201);
}
}