target(); $defaultHost = $entity instanceof Company ? $this->resolver->companyHost($entity) : $this->resolver->resellerHost($entity); return new JsonResponse([ 'scope' => $scope, 'defaultHost' => $defaultHost, 'edgeTarget' => $this->portalDomain, 'edgeIp' => $this->edgeIp(), 'domains' => array_map([$this, 'serialize'], $this->ownDomains($entity)), ]); } #[Route('/api/my-domains', name: 'my_domains_create', methods: ['POST'])] public function create(Request $request): JsonResponse { [, $entity] = $this->target(); $data = json_decode($request->getContent(), true) ?? []; $hostname = $this->normalizeHostname((string) ($data['hostname'] ?? '')); if (null === $hostname) { throw new BadRequestHttpException('Ungültiger Hostname.'); } if (null !== $this->em->getRepository(Domain::class)->findOneBy(['hostname' => $hostname])) { throw new BadRequestHttpException('Diese Domain ist bereits hinterlegt.'); } $domain = (new Domain()) ->setHostname($hostname) ->setType(Domain::TYPE_CUSTOM) ->setStatus(Domain::STATUS_PENDING); if ($entity instanceof Company) { $domain->setCompany($entity); } else { $domain->setReseller($entity); } $this->em->persist($domain); $this->em->flush(); return new JsonResponse($this->serialize($domain), 201); } #[Route('/api/my-domains/{id}/verify', name: 'my_domains_verify', methods: ['POST'])] public function verify(string $id): JsonResponse { $domain = $this->ownDomain($id); $verified = $this->dnsPointsToUs($domain->getHostname()); $domain->setStatus($verified ? Domain::STATUS_VERIFIED : Domain::STATUS_FAILED); $domain->setVerificationCheckedAt(new \DateTimeImmutable()); $this->em->flush(); return new JsonResponse([ 'domain' => $this->serialize($domain), 'ok' => $verified, 'message' => $verified ? 'Domain verifiziert. Das TLS-Zertifikat wird beim ersten Aufruf automatisch ausgestellt.' : 'DNS zeigt noch nicht auf die Plattform. Bitte CNAME/A-Eintrag prüfen (Propagierung kann dauern).', ]); } #[Route('/api/my-domains/{id}', name: 'my_domains_delete', methods: ['DELETE'])] public function delete(string $id): JsonResponse { $domain = $this->ownDomain($id); $this->em->remove($domain); $this->em->flush(); return new JsonResponse(null, 204); } /** @return array{0:string,1:Company|Reseller} */ private function target(): array { $company = $this->tenant->getCompany(); if ($company instanceof Company) { return ['company', $company]; } $reseller = $this->tenant->getReseller(); if ($reseller instanceof Reseller) { return ['reseller', $reseller]; } throw new AccessDeniedHttpException('Kein Domain-Kontext.'); } /** @return Domain[] */ private function ownDomains(Company|Reseller $entity): array { $criteria = $entity instanceof Company ? ['company' => $entity] : ['reseller' => $entity]; return $this->em->getRepository(Domain::class)->findBy($criteria, ['hostname' => 'ASC']); } private function ownDomain(string $id): Domain { $domain = $this->em->getRepository(Domain::class)->find(Uuid::fromString($id)); if (!$domain instanceof Domain) { throw new NotFoundHttpException('Domain nicht gefunden.'); } [, $entity] = $this->target(); $owns = $entity instanceof Company ? $domain->getCompany()?->getId()->equals($entity->getId()) : $domain->getReseller()?->getId()->equals($entity->getId()) && null === $domain->getCompany(); if (true !== $owns) { throw new AccessDeniedHttpException('Domain gehört nicht zum eigenen Kontext.'); } return $domain; } /** Prüft, ob der Hostname (per A oder via CNAME) auf die Plattform-Edge zeigt. */ private function dnsPointsToUs(string $hostname): bool { $hostIps = gethostbynamel($hostname); if (false === $hostIps || [] === $hostIps) { return false; } $ourIps = null !== $this->edgeIp() ? [$this->edgeIp()] : (gethostbynamel($this->portalDomain) ?: []); return [] !== array_intersect($hostIps, $ourIps); } private function edgeIp(): ?string { $ip = trim((string) $this->edgeIp); return '' !== $ip ? $ip : null; } private function normalizeHostname(string $host): ?string { $host = strtolower(trim($host)); $host = preg_replace('#^https?://#', '', $host) ?? $host; $host = rtrim($host, '/.'); if (1 !== preg_match('/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/', $host)) { return null; } return $host; } /** @return array */ private function serialize(Domain $domain): array { return [ 'id' => (string) $domain->getId(), 'hostname' => $domain->getHostname(), 'type' => $domain->getType(), 'status' => $domain->getStatus(), 'tlsStatus' => $domain->getTlsStatus(), 'checkedAt' => $domain->getVerificationCheckedAt()?->format(\DateTimeInterface::ATOM), ]; } }