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>
78 lines
2.7 KiB
PHP
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;
|
|
}
|
|
}
|