Stack & Setup
- Dockerisierte Dev-Umgebung (PHP 8.4-FPM, Nginx, MariaDB 11.4)
- Symfony 7.4 + API Platform 4.3, Doctrine ORM, LexikJWT, Messenger
- Vue 3 + TS (Vite), Vue Router, Pinia, Axios
Kern-Domäne & Auth
- Entitäten: User, PlatformPlan, Reseller, Company, Domain, Location,
Employee, ContactLink (UUIDv7)
- JWT-Login (/api/login), Rollen-Hierarchie, /api/me
- Mandantentrennung via API-Platform-Query-Extension (Lesen) +
TenantStampProcessor (Schreiben)
Öffentliche Profile (SSR)
- Profil-Landingpage, vCard-Download, QR-Code im Marken-Look
- Stabiler NFC/QR-Kurz-Link /t/{code} -> Redirect aufs aktuelle Profil
- Firmenspezifisches Branding (Farben/Logo) auf der Profilseite
Verwaltungsoberfläche (SPA)
- Brand-Look (dunkle Sidebar), rollenbasierte Navigation
- Dashboard, Reseller (+Provisioning), Firmen, Mitarbeiter, Standorte,
Domains, Design/Branding mit Live-Vorschau
Konzept & Doku: docs/KONZEPT.md (inkl. Wallet/Sync §12), README.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
2.8 KiB
PHP
80 lines
2.8 KiB
PHP
<?php
|
||
|
||
namespace App\Controller;
|
||
|
||
use App\Entity\PlatformPlan;
|
||
use App\Entity\Reseller;
|
||
use App\Entity\User;
|
||
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 – optional zusammen mit seinem Admin-Benutzer,
|
||
* damit sich der Reseller direkt einloggen kann. 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);
|
||
}
|
||
}
|
||
|
||
$adminEmail = trim((string) ($d['adminEmail'] ?? ''));
|
||
$adminPassword = (string) ($d['adminPassword'] ?? '');
|
||
$admin = null;
|
||
if ('' !== $adminEmail && '' !== $adminPassword) {
|
||
$admin = (new User())
|
||
->setEmail($adminEmail)
|
||
->setRoles([User::ROLE_RESELLER_ADMIN])
|
||
->setReseller($reseller);
|
||
$admin->setPassword($this->hasher->hashPassword($admin, $adminPassword));
|
||
}
|
||
|
||
try {
|
||
$this->em->persist($reseller);
|
||
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);
|
||
}
|
||
}
|