Skip to content

Commit 664f0dd

Browse files
Benmarkharding
authored andcommitted
[engine] Tenant bootstrapping process engine#2751
1 parent 40bb4c9 commit 664f0dd

File tree

86 files changed

+5585
-17
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+5585
-17
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Minds\Controllers\Cli\MultiTenant;
5+
6+
use Minds\Cli\Controller;
7+
use Minds\Core\Config\Config;
8+
use Minds\Core\Di\Di;
9+
use Minds\Core\EventStreams\Events\TenantBootstrapRequestEvent;
10+
use Minds\Core\EventStreams\Topics\TenantBootstrapRequestsTopic;
11+
use Minds\Core\MultiTenant\Bootstrap\Services\MultiTenantBootstrapService;
12+
use Minds\Exceptions\CliException;
13+
use Minds\Interfaces\CliControllerInterface;
14+
15+
class Bootstrap extends Controller implements CliControllerInterface
16+
{
17+
public function __construct(
18+
private ?MultiTenantBootstrapService $service = null,
19+
private ?TenantBootstrapRequestsTopic $tenantBootstrapRequestsTopic = null,
20+
) {
21+
Di::_()->get(Config::class)->set('min_log_level', 'info');
22+
$this->service ??= Di::_()->get(MultiTenantBootstrapService::class);
23+
$this->tenantBootstrapRequestsTopic ??= Di::_()->get(TenantBootstrapRequestsTopic::class);
24+
}
25+
26+
public function help($command = null)
27+
{
28+
}
29+
30+
/**
31+
* Bootstrap a new tenant.
32+
* @example
33+
* - php cli.php MultiTenant Bootstrap --tenantId=123 --siteUrl=https://www.minds.com/
34+
* @return void
35+
* @throws GraphQLException
36+
*/
37+
public function exec(): void
38+
{
39+
$tenantId = $this->getOpt('tenantId') ? (int) $this->getOpt('tenantId') : null;
40+
$siteUrl = $this->getOpt('siteUrl');
41+
$viaEventStream = $this->getOpt('viaEventStream') ?? false;
42+
43+
if (!$tenantId || $tenantId < 1) {
44+
throw new CliException('Tenant ID is a required parameter');
45+
}
46+
47+
if (!$siteUrl) {
48+
throw new CliException('Site URL is a required parameter');
49+
}
50+
51+
if (!$viaEventStream) {
52+
$this->service->bootstrap($siteUrl, $tenantId);
53+
} else {
54+
$event = (new TenantBootstrapRequestEvent())
55+
->setTenantId($tenantId)
56+
->setSiteUrl($siteUrl);
57+
$sent = $this->tenantBootstrapRequestsTopic->send($event);
58+
$this->out($sent ? 'Event sent to pulsar' : 'Event failed to send to pulsar');
59+
}
60+
}
61+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Minds\Core\EventStreams\Events;
5+
6+
use Minds\Core\EventStreams\AcknowledgmentEventTrait;
7+
use Minds\Core\EventStreams\EventInterface;
8+
use Minds\Core\EventStreams\TimebasedEventTrait;
9+
use Minds\Traits\MagicAttributes;
10+
11+
/**
12+
* @method self setTenantId(int $tenantId)
13+
* @method self setSiteUrl(string $siteUrl)
14+
* @method int getTenantId()
15+
* @method string getSiteUrl()
16+
*/
17+
class TenantBootstrapRequestEvent implements EventInterface
18+
{
19+
use MagicAttributes;
20+
use AcknowledgmentEventTrait;
21+
use TimebasedEventTrait;
22+
23+
private int $tenantId;
24+
private string $siteUrl;
25+
}

Core/EventStreams/Provider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Minds\Core\Di\Di;
99
use Minds\Core\Di\Provider as DiProvider;
1010
use Minds\Core\EventStreams\Topics\ChatNotificationsTopic;
11+
use Minds\Core\EventStreams\Topics\TenantBootstrapRequestsTopic;
1112
use Minds\Core\EventStreams\Topics\ViewsTopic;
1213
use Pulsar;
1314

@@ -57,5 +58,10 @@ public function register()
5758
ChatNotificationsTopic::class,
5859
fn (Di $di): ChatNotificationsTopic => new ChatNotificationsTopic()
5960
);
61+
62+
$this->di->bind(
63+
TenantBootstrapRequestsTopic::class,
64+
fn (Di $di): TenantBootstrapRequestsTopic => new TenantBootstrapRequestsTopic()
65+
);
6066
}
6167
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Minds\Core\EventStreams\Topics;
5+
6+
use Exception;
7+
use Minds\Core\EventStreams\EventInterface;
8+
use Minds\Core\EventStreams\Events\TenantBootstrapRequestEvent;
9+
use Pulsar\Consumer;
10+
use Pulsar\ConsumerConfiguration;
11+
use Pulsar\MessageBuilder;
12+
use Pulsar\Producer;
13+
use Pulsar\ProducerConfiguration;
14+
use Pulsar\SchemaType;
15+
16+
/**
17+
* Pulsar topic for tenant bootstrap requests.
18+
*/
19+
class TenantBootstrapRequestsTopic extends AbstractTopic implements TopicInterface
20+
{
21+
/** Topic name. */
22+
public const TOPIC = "tenant-bootstrap-requests";
23+
24+
/**
25+
* @param EventInterface $event
26+
* @return bool
27+
*/
28+
public function send(EventInterface $event): bool
29+
{
30+
if (!$event instanceof TenantBootstrapRequestEvent) {
31+
return false;
32+
}
33+
34+
$producer = $this->getProducer();
35+
36+
$result = $producer->send(
37+
(new MessageBuilder())
38+
->setEventTimestamp($event->getTimestamp() ?: time())
39+
->setContent(json_encode([
40+
'tenant_id' => $event->getTenantId(),
41+
'site_url' => $event->getSiteUrl()
42+
]))
43+
);
44+
45+
return !$result;
46+
}
47+
48+
private function getProducer(): Producer
49+
{
50+
return $this->client()->createProducer(
51+
"persistent://{$this->getPulsarTenant()}/{$this->getPulsarNamespace()}/" . self::TOPIC,
52+
(new ProducerConfiguration())
53+
->setSchema(SchemaType::JSON, self::TOPIC, $this->getSchema())
54+
);
55+
}
56+
57+
/**
58+
* @param string $subscriptionId
59+
* @param callable $callback
60+
* @param string $topicRegex
61+
* @param bool $isBatch
62+
* @param int $batchTotalAmount
63+
* @param int $execTimeoutInSeconds
64+
* @param callable|null $onBatchConsumed
65+
* @return void
66+
*/
67+
public function consume(
68+
string $subscriptionId,
69+
callable $callback,
70+
string $topicRegex = '*',
71+
bool $isBatch = false,
72+
int $batchTotalAmount = 1,
73+
int $execTimeoutInSeconds = 30,
74+
?callable $onBatchConsumed = null
75+
): void {
76+
$consumer = $this->getConsumer($subscriptionId);
77+
78+
$this->process($consumer, $callback);
79+
}
80+
81+
private function process(Consumer $consumer, callable $callback): void
82+
{
83+
while (true) {
84+
$message = $consumer->receive();
85+
try {
86+
$this->logger->info("Received message");
87+
$data = json_decode($message->getDataAsString());
88+
89+
// Map data to TenantBootstrapRequestEvent object
90+
$tenantBootstrapRequest = new TenantBootstrapRequestEvent();
91+
92+
$tenantBootstrapRequest->setTenantId($data->tenant_id);
93+
$tenantBootstrapRequest->setSiteUrl($data->site_url);
94+
95+
$this->logger->info("", [
96+
'tenant_id' => $tenantBootstrapRequest->getTenantId(),
97+
'site_url' => $tenantBootstrapRequest->getSiteUrl(),
98+
]);
99+
100+
if (call_user_func($callback, $tenantBootstrapRequest) === false) {
101+
$this->logger->info("Negative acknowledging message");
102+
$consumer->negativeAcknowledge($message);
103+
continue;
104+
}
105+
$this->logger->info("Acknowledging message");
106+
$consumer->acknowledge($message);
107+
} catch (Exception $e) {
108+
$consumer->negativeAcknowledge($message);
109+
}
110+
}
111+
}
112+
113+
/**
114+
* @param string $subscriptionId
115+
* @return Consumer
116+
*/
117+
private function getConsumer(string $subscriptionId): Consumer
118+
{
119+
return $this->client()->subscribeWithRegex(
120+
"persistent://{$this->getPulsarTenant()}/{$this->getPulsarNamespace()}/" . self::TOPIC,
121+
$subscriptionId,
122+
(new ConsumerConfiguration())
123+
->setConsumerType(Consumer::ConsumerShared)
124+
->setSchema(SchemaType::JSON, self::TOPIC, $this->getSchema(), [])
125+
);
126+
}
127+
128+
/**
129+
* @return string
130+
*/
131+
private function getSchema(): string
132+
{
133+
return json_encode([
134+
'type' => 'record',
135+
'name' => 'TenantBootstrapRequest',
136+
'namespace' => $this->getPulsarNamespace(),
137+
'fields' => [
138+
[
139+
'name' => 'tenant_id',
140+
'type' => [ 'null', 'int' ],
141+
],
142+
[
143+
'name' => 'site_url',
144+
'type' => [ 'null', 'string' ],
145+
]
146+
]
147+
]);
148+
}
149+
}

Core/Feeds/Activity/RichEmbed/Metascraper/Metadata.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* @method self setTitle(string $title)
2020
* @method string|null getAuthor()
2121
* @method self setAuthor(string $author)
22+
* @method string|null getPublisher()
23+
* @method self setPublisher(string $publisher)
2224
* @method string|null getImage()
2325
* @method self setImage(string $image)
2426
* @method string|null getLogo()
@@ -47,6 +49,9 @@ class Metadata implements ExportableInterface, JsonSerializable
4749
/** @var string|null author from metadata. */
4850
protected ?string $author;
4951

52+
/** @var string|null publisher from metadata. */
53+
protected ?string $publisher;
54+
5055
/** @var string|null image url from metadata. */
5156
protected ?string $image;
5257

@@ -68,6 +73,7 @@ class Metadata implements ExportableInterface, JsonSerializable
6873
public function fromMetascraperData(array $data, ?string $url = null): self
6974
{
7075
$this->setUrl($url ?? $data['url'] ?? '')
76+
->setPublisher($data['publisher'] ?? '')
7177
->setCanonicalUrl($data['url'] ?? '')
7278
->setDescription($data['description'] ?? '')
7379
->setTitle($data['title'] ?? '')
@@ -116,7 +122,8 @@ public function export(array $extras = []): array
116122
]
117123
],
118124
'date' => $this->date,
119-
'html' => $this->iframe
125+
'html' => $this->iframe,
126+
'publisher' => $this->publisher
120127
];
121128
}
122129
}

Core/MultiTenant/AutoLogin/AutoLoginService.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,21 @@ public function buildLoginUrlFromTenant(
6464
/**
6565
* Build the URL for autologin from tenant to be used with query params, not POST
6666
* @param Tenant $tenant - the tenant.
67+
* @param string|null $redirectPath - Path to redirect a user to upon login.
6768
* @return string - login URL.
6869
*/
6970
public function buildLoginUrlWithParamsFromTenant(
7071
Tenant $tenant,
7172
User $loggedInUser,
73+
?string $redirectPath = null
7274
): string {
7375
$domain = $this->tenantDomainService->buildNavigatableDomain($tenant);
7476
$jwtToken = $this->buildJwtToken($tenant->id, $loggedInUser);
75-
return "https://$domain/api/v3/multi-tenant/auto-login/login?token=$jwtToken";
77+
78+
return "https://$domain/api/v3/multi-tenant/auto-login/login?". http_build_query([
79+
'token' => $jwtToken,
80+
'redirect_path' => $redirectPath
81+
]);
7682
}
7783

7884
/**

Core/MultiTenant/AutoLogin/Controller.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,13 @@ public function getLoginUrl(ServerRequest $request): JsonResponse
5050
public function postLogin(ServerRequest $request): RedirectResponse
5151
{
5252
$jwtToken = $request->getParsedBody()['jwt_token'];
53+
$redirectPath = $this->getSanitizedRedirectPath(
54+
$request->getParsedBody()['redirect_path'] ?? null
55+
);
5356

5457
$this->autoLoginService->performLogin($jwtToken);
5558

56-
return new RedirectResponse('/network/admin');
59+
return new RedirectResponse($redirectPath);
5760
}
5861

5962

@@ -63,10 +66,24 @@ public function postLogin(ServerRequest $request): RedirectResponse
6366
public function getLogin(ServerRequest $request): RedirectResponse
6467
{
6568
$jwtToken = $request->getQueryParams()['token'];
69+
$redirectPath = $this->getSanitizedRedirectPath(
70+
$request->getQueryParams()['redirect_path'] ?? null
71+
);
6672

6773
$this->autoLoginService->performLogin($jwtToken);
6874

69-
return new RedirectResponse('/network/admin');
75+
return new RedirectResponse($redirectPath);
7076
}
7177

78+
/**
79+
* Gets sanitized redirect path.
80+
* @param string|null $redirectPath - the path to sanitize.
81+
* @return string - the redirect path.
82+
*/
83+
private function getSanitizedRedirectPath(?string $redirectPath): string
84+
{
85+
return $redirectPath && str_starts_with($redirectPath, '/') ?
86+
$redirectPath :
87+
'/network/admin';
88+
}
7289
}

0 commit comments

Comments
 (0)