diff --git a/.env.example b/.env.example index a117d6925..e3309971c 100644 --- a/.env.example +++ b/.env.example @@ -266,6 +266,11 @@ PAYMENTS_SERVICE_OAUTH2_CLIENT_ID= PAYMENTS_SERVICE_OAUTH2_CLIENT_SECRET= PAYMENTS_SERVICE_OAUTH2_SCOPES=payment-profile/read +# DROPBOX MATERIALIZER SERVICE + +DROPBOX_MATERIALIZER_URL=http://localhost:8100 +DROPBOX_MATERIALIZER_KEY= +DROPBOX_MATERIALIZER_TIMEOUT=10 # L5_FORMAT_TO_USE_FOR_DOCS=yaml # L5_SWAGGER_GENERATE_ALWAYS=true # Dev setting diff --git a/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitValidationRulesFactory.php b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitValidationRulesFactory.php index deb1c6965..f1fa5b2af 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitValidationRulesFactory.php +++ b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitValidationRulesFactory.php @@ -98,6 +98,7 @@ public static function buildForAdd(array $payload = []): array 'registration_send_order_email_automatically' => 'sometimes|boolean', 'registration_allow_automatic_reminder_emails' => 'sometimes|boolean', 'allow_update_attendee_extra_questions' => 'sometimes|boolean', + 'dropbox_sync_enabled' => 'sometimes|boolean', 'time_zone_label' => 'sometimes|string', 'registration_allowed_refund_request_till_date' => 'nullable|date_format:U|epoch_seconds', 'registration_slug_prefix' => 'required|string|max:50', @@ -181,6 +182,7 @@ public static function buildForUpdate(array $payload = []): array 'registration_send_order_email_automatically' => 'sometimes|boolean', 'registration_allow_automatic_reminder_emails' => 'sometimes|boolean', 'allow_update_attendee_extra_questions' => 'sometimes|boolean', + 'dropbox_sync_enabled' => 'sometimes|boolean', 'time_zone_label' => 'sometimes|string', 'registration_allowed_refund_request_till_date' => 'nullable|date_format:U|epoch_seconds', 'registration_slug_prefix' => 'sometimes|string|max:50', diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitDropboxSyncApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitDropboxSyncApiController.php new file mode 100644 index 000000000..4ddf485ce --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitDropboxSyncApiController.php @@ -0,0 +1,187 @@ +repository = $summit_repository; + $this->materializer_api = $materializer_api; + } + + /** + * @param Summit $summit + * @return void + * @throws \Exception + */ + private function checkAdminPermission(Summit $summit): void + { + $current_member = $this->resource_server_context->getCurrentUser(); + if (!is_null($current_member) && !$current_member->isAdmin() && !$current_member->hasPermissionForOnGroup($summit, IGroup::SummitAdministrators)) + throw new AuthzException( + sprintf("Member %s has not permission for this Summit", $current_member->getId()) + ); + } + + /** + * @param int $summit_id + * @return Summit + * @throws EntityNotFoundException + */ + private function findSummit(int $summit_id): Summit + { + $summit = $this->repository->getById($summit_id); + if (is_null($summit) || !$summit instanceof Summit) + throw new EntityNotFoundException(sprintf("Summit %s not found", $summit_id)); + return $summit; + } + + /** + * @param Summit $summit + * @throws ValidationException + */ + private function requireSyncEnabled(Summit $summit): void + { + if (!$summit->isDropboxSyncEnabled()) + throw new ValidationException("Dropbox sync is not enabled for this summit."); + } + + /** + * POST /api/v1/summits/{id}/dropbox-sync/materialize + */ + public function materialize($summit_id) + { + return $this->processRequest(function () use ($summit_id) { + $summit = $this->findSummit(intval($summit_id)); + $this->checkAdminPermission($summit); + $this->requireSyncEnabled($summit); + + $result = $this->materializer_api->materialize($summit->getId()); + return $this->ok($result); + }); + } + + /** + * POST /api/v1/summits/{id}/dropbox-sync/materialize/{location_id}/{room_id} + */ + public function materializeRoom($summit_id, $location_id, $room_id) + { + return $this->processRequest(function () use ($summit_id, $location_id, $room_id) { + $summit = $this->findSummit(intval($summit_id)); + $this->checkAdminPermission($summit); + $this->requireSyncEnabled($summit); + + $venue = $summit->getLocation(intval($location_id)); + if (is_null($venue) || !$venue instanceof SummitVenue) + throw new EntityNotFoundException(sprintf("Venue %s not found", $location_id)); + + $room = $venue->getRoom(intval($room_id)); + if (is_null($room)) + throw new EntityNotFoundException(sprintf("Room %s not found", $room_id)); + + $result = $this->materializer_api->materializeRoom( + $summit->getId(), + $venue->getName(), + $room->getName() + ); + return $this->ok($result); + }); + } + + /** + * POST /api/v1/summits/{id}/dropbox-sync/backfill + */ + public function backfill($summit_id) + { + return $this->processRequest(function () use ($summit_id) { + $summit = $this->findSummit(intval($summit_id)); + $this->checkAdminPermission($summit); + $this->requireSyncEnabled($summit); + + $result = $this->materializer_api->backfill($summit->getId()); + return $this->ok($result); + }); + } + + /** + * POST /api/v1/summits/{id}/dropbox-sync/rebuild + */ + public function rebuild($summit_id) + { + return $this->processRequest(function () use ($summit_id) { + $summit = $this->findSummit(intval($summit_id)); + $this->checkAdminPermission($summit); + + $result = $this->materializer_api->rebuild($summit->getId()); + return $this->ok($result); + }); + } + + /** + * GET /api/v1/summits/{id}/dropbox-sync/preflight + */ + public function preflight($summit_id) + { + return $this->processRequest(function () use ($summit_id) { + $summit = $this->findSummit(intval($summit_id)); + $this->checkAdminPermission($summit); + + $result = $this->materializer_api->preflight($summit->getId()); + return $this->ok($result); + }); + } + + /** + * GET /api/v1/summits/{id}/dropbox-sync/status + */ + public function status($summit_id) + { + return $this->processRequest(function () use ($summit_id) { + $summit = $this->findSummit(intval($summit_id)); + $this->checkAdminPermission($summit); + + $result = $this->materializer_api->status($summit->getId()); + return $this->ok($result); + }); + } +} diff --git a/app/ModelSerializers/Summit/SummitSerializer.php b/app/ModelSerializers/Summit/SummitSerializer.php index 45f71321c..ff3f165fa 100644 --- a/app/ModelSerializers/Summit/SummitSerializer.php +++ b/app/ModelSerializers/Summit/SummitSerializer.php @@ -91,6 +91,7 @@ class SummitSerializer extends SilverStripeSerializer 'RegistrationAllowAutomaticReminderEmails' => 'registration_allow_automatic_reminder_emails:json_boolean', 'Modality' => 'modality:json_string', 'AllowUpdateAttendeeExtraQuestions' => 'allow_update_attendee_extra_questions:json_boolean', + 'DropboxSyncEnabled' => 'dropbox_sync_enabled:json_boolean', 'TimeZoneLabel' => 'time_zone_label:json_string', 'RegistrationAllowedRefundRequestTillDate' => 'registration_allowed_refund_request_till_date:datetime_epoch', 'RegistrationSlugPrefix' => 'registration_slug_prefix:json_string', @@ -162,6 +163,7 @@ class SummitSerializer extends SilverStripeSerializer 'registration_allow_automatic_reminder_emails', 'modality', 'allow_update_attendee_extra_questions', + 'dropbox_sync_enabled', 'time_zone_label', 'registration_allowed_refund_request_till_date', 'registration_slug_prefix', diff --git a/app/Models/Foundation/Summit/Factories/SummitFactory.php b/app/Models/Foundation/Summit/Factories/SummitFactory.php index b7289fadf..05e4e0d84 100644 --- a/app/Models/Foundation/Summit/Factories/SummitFactory.php +++ b/app/Models/Foundation/Summit/Factories/SummitFactory.php @@ -83,6 +83,10 @@ public static function populate(Summit $summit, array $data){ $summit->setAllowUpdateAttendeeExtraQuestions(boolval($data['allow_update_attendee_extra_questions'])); } + if(isset($data['dropbox_sync_enabled'])){ + $summit->setDropboxSyncEnabled(boolval($data['dropbox_sync_enabled'])); + } + if(isset($data['dates_label']) ){ $summit->setDatesLabel(trim($data['dates_label'])); } diff --git a/app/Models/Foundation/Summit/Summit.php b/app/Models/Foundation/Summit/Summit.php index c10a6b3e9..c054742d1 100644 --- a/app/Models/Foundation/Summit/Summit.php +++ b/app/Models/Foundation/Summit/Summit.php @@ -498,6 +498,12 @@ public function setMarketingSiteOauth2ClientScopes(string $marketing_site_oauth2 #[ORM\Column(name: 'RegistrationAllowAutomaticReminderEmails', type: 'boolean')] private $registration_allow_automatic_reminder_emails; + /** + * @var bool + */ + #[ORM\Column(name: 'DropboxSyncEnabled', type: 'boolean')] + private $dropbox_sync_enabled; + #[ORM\OneToMany(targetEntity: \SummitEvent::class, mappedBy: 'summit', cascade: ['persist', 'remove'], orphanRemoval: true, fetch: 'EXTRA_LAZY')] private $events; @@ -1190,6 +1196,7 @@ public function __construct() $this->registration_allow_automatic_reminder_emails = true; $this->registration_send_order_email_automatically = true; $this->allow_update_attendee_extra_questions = false; + $this->dropbox_sync_enabled = false; $this->registration_companies = new ArrayCollection(); $this->external_registration_feed_last_ingest_date = null; $this->speakers_announcement_emails = new ArrayCollection(); @@ -6362,6 +6369,22 @@ public function setAllowUpdateAttendeeExtraQuestions(bool $allow_update_attendee $this->allow_update_attendee_extra_questions = $allow_update_attendee_extra_questions; } + /** + * @return bool + */ + public function isDropboxSyncEnabled(): bool + { + return $this->dropbox_sync_enabled; + } + + /** + * @param bool $dropbox_sync_enabled + */ + public function setDropboxSyncEnabled(bool $dropbox_sync_enabled): void + { + $this->dropbox_sync_enabled = $dropbox_sync_enabled; + } + /** * @return bool */ diff --git a/app/Services/Apis/DropboxMaterializerApi.php b/app/Services/Apis/DropboxMaterializerApi.php new file mode 100644 index 000000000..d85e4a370 --- /dev/null +++ b/app/Services/Apis/DropboxMaterializerApi.php @@ -0,0 +1,159 @@ +push(GuzzleRetryMiddleware::factory([ + 'retry_on_methods' => ['GET'], + ])); + + $this->client = new Client([ + 'handler' => $stack, + 'base_uri' => Config::get('dropbox_materializer.base_url', 'http://localhost:8100'), + 'timeout' => Config::get('dropbox_materializer.timeout', 10), + 'allow_redirects' => false, + 'verify' => Config::get('curl.verify_ssl_cert', true), + ]); + + $this->internal_key = Config::get('dropbox_materializer.internal_key', ''); + } + + /** + * @return array + */ + private function headers(): array + { + return [ + 'X-Internal-Key' => $this->internal_key, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ]; + } + + /** + * @param string $method + * @param string $uri + * @return array + */ + private function request(string $method, string $uri): array + { + try { + $response = $this->client->request($method, $uri, [ + 'headers' => $this->headers(), + ]); + + $body = $response->getBody()->getContents(); + return json_decode($body, true) ?? []; + } catch (RequestException $ex) { + Log::warning( + sprintf( + "DropboxMaterializerApi::request %s %s error: %s", + $method, + $uri, + $ex->getMessage() + ) + ); + + $response = $ex->getResponse(); + if ($response) { + $body = $response->getBody()->getContents(); + $decoded = json_decode($body, true); + return $decoded ?? ['error' => $ex->getMessage(), 'status' => $response->getStatusCode()]; + } + + return ['error' => $ex->getMessage()]; + } + } + + /** + * @param int $summitId + * @return array + */ + public function materialize(int $summitId): array + { + return $this->request('POST', "/api/sync/materialize/{$summitId}/"); + } + + /** + * @param int $summitId + * @param string $venue + * @param string $room + * @return array + */ + public function materializeRoom(int $summitId, string $venue, string $room): array + { + $venue = rawurlencode($venue); + $room = rawurlencode($room); + return $this->request('POST', "/api/sync/materialize/{$summitId}/{$venue}/{$room}/"); + } + + /** + * @param int $summitId + * @return array + */ + public function backfill(int $summitId): array + { + return $this->request('POST', "/api/sync/backfill/{$summitId}/"); + } + + /** + * @param int $summitId + * @return array + */ + public function rebuild(int $summitId): array + { + return $this->request('POST', "/api/sync/rebuild/{$summitId}/"); + } + + /** + * @param int $summitId + * @return array + */ + public function preflight(int $summitId): array + { + return $this->request('GET', "/api/sync/preflight/{$summitId}/"); + } + + /** + * @param int $summitId + * @return array + */ + public function status(int $summitId): array + { + return $this->request('GET', "/api/sync/status/{$summitId}/"); + } +} diff --git a/app/Services/Apis/IDropboxMaterializerApi.php b/app/Services/Apis/IDropboxMaterializerApi.php new file mode 100644 index 000000000..1a318299c --- /dev/null +++ b/app/Services/Apis/IDropboxMaterializerApi.php @@ -0,0 +1,58 @@ + env('DROPBOX_MATERIALIZER_URL', 'http://localhost:8100'), + 'internal_key' => env('DROPBOX_MATERIALIZER_KEY', ''), + 'timeout' => env('DROPBOX_MATERIALIZER_TIMEOUT', 10), +]; diff --git a/database/migrations/model/Version20260318120000.php b/database/migrations/model/Version20260318120000.php new file mode 100644 index 000000000..317e8d2bd --- /dev/null +++ b/database/migrations/model/Version20260318120000.php @@ -0,0 +1,40 @@ +addSql($sql); + } + + public function down(Schema $schema): void + { + $this->addSql("ALTER TABLE Summit DROP COLUMN DropboxSyncEnabled"); + } +} diff --git a/database/migrations/model/initial_migrations.sql b/database/migrations/model/initial_migrations.sql index db60205bc..bcd41673a 100644 --- a/database/migrations/model/initial_migrations.sql +++ b/database/migrations/model/initial_migrations.sql @@ -342,4 +342,5 @@ INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migratio INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Model\\Version20240320151845', '2024-04-11 16:52:02'); INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Model\\Version20240326133631', '2024-04-11 16:52:06'); INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Model\\Version20240326133636', '2024-04-11 16:52:06'); -INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Model\\Version20240410135620', '2024-04-11 16:52:06'); \ No newline at end of file +INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Model\\Version20240410135620', '2024-04-11 16:52:06'); +INSERT INTO DoctrineMigration (version, executed_at) VALUES ('Database\\Migrations\\Model\\Version20260318120000', '2026-03-18 12:00:00'); \ No newline at end of file diff --git a/database/migrations/model/initial_schema.sql b/database/migrations/model/initial_schema.sql index d93e02c0c..b4bbfa43c 100644 --- a/database/migrations/model/initial_schema.sql +++ b/database/migrations/model/initial_schema.sql @@ -9129,6 +9129,7 @@ create table Summit SecondaryLogoID int null, SpeakersSupportEmail varchar(255) null, MarkAsDeleted tinyint unsigned default '0' not null, + DropboxSyncEnabled tinyint(1) default 0 not null, constraint QRCodesEncKey unique (QRCodesEncKey), constraint Summit_RegistrationSlugPrefix diff --git a/routes/api_v1.php b/routes/api_v1.php index 6c9d66b20..510bf0083 100644 --- a/routes/api_v1.php +++ b/routes/api_v1.php @@ -277,6 +277,17 @@ ), 'uses' => 'OAuth2SummitApiController@getSummit'])->where('id', 'current|[0-9]+'); + // dropbox sync proxy routes + + Route::group(['prefix' => 'dropbox-sync'], function () { + Route::post('materialize', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitDropboxSyncApiController@materialize']); + Route::post('materialize/{location_id}/{room_id}', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitDropboxSyncApiController@materializeRoom']); + Route::post('backfill', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitDropboxSyncApiController@backfill']); + Route::post('rebuild', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitDropboxSyncApiController@rebuild']); + Route::get('preflight', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitDropboxSyncApiController@preflight']); + Route::get('status', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitDropboxSyncApiController@status']); + }); + // selection plan extra questions ( by summit ) Route::group(['prefix' => 'selection-plan-extra-questions'], function () {