serialize()); } #[Route('/api/my-branding', name: 'my_branding_put', methods: ['PUT'])] #[IsGranted('ROLE_COMPANY_ADMIN')] public function put(Request $request): JsonResponse { [, $entity] = $this->target(); $data = json_decode($request->getContent(), true) ?? []; $cfg = $entity->getBrandingConfig(); foreach (['primaryColor', 'primaryColorDark', 'primaryColorSoft'] as $key) { if (\array_key_exists($key, $data)) { $hex = $this->hex($data[$key] ?? null); if (null !== $hex) { $cfg[$key] = $hex; } else { unset($cfg[$key]); } } } if (\array_key_exists('tagline', $data)) { $tag = trim((string) ($data['tagline'] ?? '')); if ('' !== $tag) { $cfg['tagline'] = mb_substr($tag, 0, 120); } else { unset($cfg['tagline']); } } $entity->setBrandingConfig($cfg); $this->em->flush(); return new JsonResponse($this->serialize()); } #[Route('/api/my-branding/logo', name: 'my_branding_logo_post', methods: ['POST'])] #[IsGranted('ROLE_COMPANY_ADMIN')] public function uploadLogo(Request $request): JsonResponse { [$scope, $entity] = $this->target(); $file = $request->files->get('file'); if (!$file instanceof UploadedFile) { throw new BadRequestHttpException('Keine Datei (Feld "file").'); } $ext = strtolower((string) $file->getClientOriginalExtension()); if (!\in_array($ext, ['png', 'jpg', 'jpeg', 'svg'], true)) { throw new BadRequestHttpException('Nur PNG, JPG oder SVG erlaubt.'); } $cfg = $entity->getBrandingConfig(); $old = \is_string($cfg['logoKey'] ?? null) ? $cfg['logoKey'] : null; $key = sprintf('branding/%s-%s/logo-%s.%s', $scope, $entity->getId()->toRfc4122(), bin2hex(random_bytes(4)), $ext); $this->cardAssets->write($key, (string) file_get_contents($file->getPathname())); if (null !== $old && $old !== $key && $this->cardAssets->fileExists($old)) { $this->cardAssets->delete($old); } $cfg['logoKey'] = $key; $entity->setBrandingConfig($cfg); $this->em->flush(); return new JsonResponse($this->serialize(), 201); } #[Route('/api/my-branding/logo', name: 'my_branding_logo_delete', methods: ['DELETE'])] #[IsGranted('ROLE_COMPANY_ADMIN')] public function deleteLogo(): JsonResponse { [, $entity] = $this->target(); $cfg = $entity->getBrandingConfig(); $key = \is_string($cfg['logoKey'] ?? null) ? $cfg['logoKey'] : null; if (null !== $key && $this->cardAssets->fileExists($key)) { $this->cardAssets->delete($key); } unset($cfg['logoKey']); $entity->setBrandingConfig($cfg); $this->em->flush(); return new JsonResponse($this->serialize()); } /** Öffentliche Logo-Auslieferung (vom Branding-Endpoint verlinkt). */ #[Route('/api/branding/logo/{scope}/{id}.png', name: 'branding_logo', methods: ['GET'])] public function logo(string $scope, string $id): Response { $entity = $this->load($scope, $id); $key = \is_string($entity->getBrandingConfig()['logoKey'] ?? null) ? $entity->getBrandingConfig()['logoKey'] : null; if (null === $key || !$this->cardAssets->fileExists($key)) { throw new NotFoundHttpException('Kein Logo.'); } $bytes = $this->cardAssets->read($key); $mime = str_ends_with($key, '.svg') ? 'image/svg+xml' : (str_ends_with($key, '.png') ? 'image/png' : 'image/jpeg'); return new Response($bytes, 200, ['Content-Type' => $mime, 'Cache-Control' => 'public, max-age=86400']); } /** @return array{0:string,1:Company|Reseller} */ private function target(): array { $company = $this->tenant->getCompany(); if ($company instanceof Company) { return [BrandingService::SCOPE_COMPANY, $company]; } $reseller = $this->tenant->getReseller(); if ($reseller instanceof Reseller) { return [BrandingService::SCOPE_RESELLER, $reseller]; } throw new AccessDeniedHttpException('Kein Branding-Kontext.'); } private function load(string $scope, string $id): Company|Reseller { $uuid = Uuid::fromString($id); $entity = BrandingService::SCOPE_COMPANY === $scope ? $this->em->getRepository(Company::class)->find($uuid) : $this->em->getRepository(Reseller::class)->find($uuid); if (!$entity instanceof Company && !$entity instanceof Reseller) { throw new NotFoundHttpException('Nicht gefunden.'); } return $entity; } /** @return array */ private function serialize(): array { [$scope, $entity] = $this->target(); $cfg = $entity->getBrandingConfig(); $tenant = $entity instanceof Company ? new \App\Service\ResolvedTenant(\App\Service\ResolvedTenant::KIND_COMPANY, $entity->getReseller(), $entity) : new \App\Service\ResolvedTenant(\App\Service\ResolvedTenant::KIND_RESELLER, $entity); $host = $entity instanceof Company ? $this->resolver->companyHost($entity) : $this->resolver->resellerHost($entity); return [ 'scope' => $scope, 'name' => $entity->getName(), 'address' => $host, 'primaryColor' => $cfg['primaryColor'] ?? '#f58220', 'primaryColorDark' => $cfg['primaryColorDark'] ?? '#d96500', 'primaryColorSoft' => $cfg['primaryColorSoft'] ?? '#fff2e7', 'tagline' => $cfg['tagline'] ?? '', 'hasLogo' => '' !== (string) ($cfg['logoKey'] ?? ''), 'logoUrl' => $this->branding->logoUrl($tenant), ]; } private function hex(mixed $value): ?string { return (\is_string($value) && preg_match('/^#[0-9a-fA-F]{6}$/', $value)) ? strtolower($value) : null; } }