vcard4reseller/backend/src/Controller/CompanyBrandingController.php
Thomas Peterson ebaf509a2f Fundament: Symfony+API-Platform-Backend & Vue-SPA (Phase 0–2)
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>
2026-05-31 11:12:53 +02:00

78 lines
2.7 KiB
PHP

<?php
namespace App\Controller;
use App\Entity\Company;
use App\Security\TenantContext;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Uid\Uuid;
/**
* Branding (Logo/Farben) einer Firma setzen. Erlaubt Firmen-Admins die
* Pflege ihres eigenen Brandings, ohne die Firma selbst ändern zu können.
*/
#[IsGranted('ROLE_COMPANY_ADMIN')]
final class CompanyBrandingController
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly TenantContext $tenant,
) {
}
#[Route('/api/companies/{id}/branding', name: 'company_branding', methods: ['PATCH'])]
public function __invoke(string $id, Request $request): JsonResponse
{
$company = $this->em->getRepository(Company::class)->find(Uuid::fromString($id));
if (!$company instanceof Company) {
throw new NotFoundHttpException('Firma nicht gefunden.');
}
$this->assertAccess($company);
$data = json_decode($request->getContent(), true) ?? [];
$company->setBrandingConfig($this->sanitize($data));
$this->em->flush();
return new JsonResponse($company->getBrandingConfig());
}
private function assertAccess(Company $company): void
{
if ($this->tenant->isPlatformAdmin()) {
return;
}
$reseller = $this->tenant->getReseller();
if (null === $reseller || $company->getReseller()?->getId()->equals($reseller->getId()) !== true) {
throw new AccessDeniedHttpException('Firma gehört nicht zum eigenen Mandanten.');
}
$own = $this->tenant->getCompany();
if (null !== $own && !$company->getId()->equals($own->getId())) {
throw new AccessDeniedHttpException('Nur die eigene Firma darf bearbeitet werden.');
}
}
/** Nur erlaubte, validierte Felder übernehmen (verhindert CSS-Injection). */
private function sanitize(array $data): array
{
$out = [];
foreach (['primaryColor', 'primaryDark'] as $key) {
$val = (string) ($data[$key] ?? '');
if (preg_match('/^#[0-9a-fA-F]{6}$/', $val)) {
$out[$key] = $val;
}
}
$logo = (string) ($data['logoUrl'] ?? '');
if (str_starts_with($logo, 'https://') || str_starts_with($logo, '/')) {
$out['logoUrl'] = $logo;
}
return $out;
}
}