turnstile

Drupal paywall plugin
Log | Files | Refs | README | LICENSE

commit a06c174c518b0561e44fc3c5001746bae9293c86
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed, 23 Jul 2025 23:53:54 +0200

initial sketch

Diffstat:
AREADME.md | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig/install/turnstile.settings.yml | 5+++++
Aconfig/schema/turnstile.schema.yml | 16++++++++++++++++
Asrc/Form/TurnstileSettingsForm.php | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Service/TurnstileService.php | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aturnstile.info.yml | 10++++++++++
Aturnstile.install | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aturnstile.links.menu.yml | 6++++++
Aturnstile.module | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aturnstile.permissions.yml | 5+++++
Aturnstile.routing.yml | 10++++++++++
Aturnstile.services.yml | 4++++
12 files changed, 870 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -0,0 +1,77 @@ +# Turnstile + +A Drupal module that adds paywall functionality to nodes with configurable content transformation. + +## Features + +- Adds a "price" field to configurable content types +- Implements paywall protection with cookie-based access control +- Transforms content for unpaid users (currently converts to lowercase) +- Configurable content types for paywall protection +- Integration with external payment backend +- Admin interface for configuration + +## Installation + +1. Download and extract the module to your `modules/custom/` directory +2. Enable the module via Drush: `drush en turnstile` +3. Or enable via the Drupal admin interface at `/admin/modules` + +## Configuration + +Navigate to `/admin/config/content/Turnstile` to configure: + +- **Enabled Content Types**: Select which content types should have the price field and paywall protection +- **Payment Backend URL**: HTTP URL for your payment backend service +- **Access Token**: Authentication token for the payment backend + +## Usage + +1. Create or edit a node of an enabled content type +2. Set a price in the "Price" field to enable paywall protection +3. Users without the `payment_cookie` will see transformed content (lowercase) +4. Users with the cookie will see the original content + +## How it Works + +The module uses `hook_entity_view_alter()` to intercept node rendering and calls the `turnstile_check_paywall_cookie()` function to: + +1. Check if a `payment_cookie` is set +2. If not set, transform the content body to lowercase +3. If set, display the original content + +## Extending the Module + +You can modify the `turnstile_check_paywall_cookie()` function in `turnstile.module` to implement different transformation logic or integrate with your payment system. + +The `Turnstile` class provides methods for backend payment verification and can be extended for more complex payment workflows. + +## File Structure + +``` +turnstile/ +├── config/ +│ └── install/ +│ └── turnstile.settings.yml +├── src/ +│ ├── Form/ +│ │ └── TurnstileSettingsForm.php +│ └── Service/ +│ └── Turnstile.php +├── turnstile.info.yml +├── turnstile.install +├── turnstile.module +├── turnstile.permissions.yml +├── turnstile.routing.yml +├── turnstile.services.yml +└── README.md +``` + +## Requirements + +- Drupal 9 or 10 +- PHP 7.4 or higher + +## License + +This module is provided as-is for demonstration purposes. +\ No newline at end of file diff --git a/config/install/turnstile.settings.yml b/config/install/turnstile.settings.yml @@ -0,0 +1,4 @@ +enabled_content_types: + - article +payment_backend_url: '' +access_token: '' +\ No newline at end of file diff --git a/config/schema/turnstile.schema.yml b/config/schema/turnstile.schema.yml @@ -0,0 +1,16 @@ +turnstile.settings: + type: config_object + label: 'Paywall Guard settings' + mapping: + enabled_content_types: + type: sequence + label: 'Enabled content types' + sequence: + type: string + label: 'Content type' + payment_backend_url: + type: string + label: 'Payment backend URL' + access_token: + type: string + label: 'Access token' diff --git a/src/Form/TurnstileSettingsForm.php b/src/Form/TurnstileSettingsForm.php @@ -0,0 +1,312 @@ +<?php + +namespace Drupal\turnstile\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\field\Entity\FieldConfig; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Configure Turnstile settings. + */ +class TurnstileSettingsForm extends ConfigFormBase { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a TurnstileSettingsForm object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager) { + parent::__construct($config_factory); + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['turnstile.settings']; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'turnstile_settings_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('turnstile.settings'); + + // Get all available content types. + $content_types = $this->entityTypeManager->getStorage('node_type')->loadMultiple(); + $type_options = []; + foreach ($content_types as $type) { + $type_options[$type->id()] = $type->label(); + } + + $form['enabled_content_types'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Enabled Content Types'), + '#description' => $this->t('Select which content types should have the price field and be subject to paywall transformation.'), + '#options' => $type_options, + '#default_value' => $config->get('enabled_content_types') ?: [], + ]; + + $form['payment_backend_url'] = [ + '#type' => 'url', + '#title' => $this->t('Payment Backend URL'), + '#description' => $this->t('HTTP URL for the payment backend service.'), + '#default_value' => $config->get('payment_backend_url') ?: '', + '#maxlength' => 255, + ]; + + $form['access_token'] = [ + '#type' => 'textfield', + '#title' => $this->t('Access Token'), + '#description' => $this->t('Access token for authenticating with the payment backend.'), + '#default_value' => $config->get('access_token') ?: '', + '#maxlength' => 255, + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $this->config('turnstile.settings'); + + // Get old and new content types. + $old_enabled_types = $config->get('enabled_content_types') ?: []; + $new_enabled_types = array_filter($form_state->getValue('enabled_content_types')); + $new_enabled_types = array_values($new_enabled_types); + + // Find content types to add and remove. + $types_to_add = array_diff($new_enabled_types, $old_enabled_types); + $types_to_remove = array_diff($old_enabled_types, $new_enabled_types); + + // Add fields to newly enabled content types. + if (!empty($types_to_add)) { + $this->addFieldsToContentTypes($types_to_add); + } + + // Remove fields from disabled content types. + if (!empty($types_to_remove)) { + $this->removeFieldsFromContentTypes($types_to_remove); + } + + // Save configuration. + $config->set('enabled_content_types', $new_enabled_types); + $config->set('payment_backend_url', $form_state->getValue('payment_backend_url')); + $config->set('access_token', $form_state->getValue('access_token')); + $config->save(); + + parent::submitForm($form, $form_state); + + // Display summary of changes. + if (!empty($types_to_add) || !empty($types_to_remove)) { + $this->displayFieldChanges($types_to_add, $types_to_remove); + } + } + + /** + * Add price fields to content types. + * + * @param array $bundles + * Array of content type machine names. + */ + protected function addFieldsToContentTypes(array $bundles) { + // Ensure field storage exists. + $field_storage = FieldStorageConfig::loadByName('node', 'field_price'); + if (!$field_storage) { + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_price', + 'entity_type' => 'node', + 'type' => 'string', + 'cardinality' => 1, + 'settings' => [ + 'max_length' => 255, + ], + ]); + $field_storage->save(); + } + + foreach ($bundles as $bundle) { + // Verify content type exists. + if (!$this->entityTypeManager->getStorage('node_type')->load($bundle)) { + continue; + } + + // Check if field already exists for this bundle. + $existing_field = FieldConfig::loadByName('node', $bundle, 'field_price'); + if ($existing_field) { + continue; + } + + // Create field configuration. + $field_config = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => $bundle, + 'label' => 'Price', + 'description' => 'Price for accessing this content', + 'required' => FALSE, + ]); + $field_config->save(); + + // Add to form display. + $form_display = EntityFormDisplay::load('node.' . $bundle . '.default'); + if ($form_display) { + $form_display->setComponent('field_price', [ + 'type' => 'string_textfield', + 'weight' => 10, + 'settings' => [ + 'size' => 60, + 'placeholder' => '', + ], + ]); + $form_display->save(); + } + + // Add to view display. + $view_display = EntityViewDisplay::load('node.' . $bundle . '.default'); + if ($view_display) { + $view_display->setComponent('field_price', [ + 'type' => 'string', + 'weight' => 10, + 'label' => 'above', + ]); + $view_display->save(); + } + } + } + + /** + * Remove price fields from content types. + * + * @param array $bundles + * Array of content type machine names. + */ + protected function removeFieldsFromContentTypes(array $bundles) { + foreach ($bundles as $bundle) { + $field_config = FieldConfig::loadByName('node', $bundle, 'field_price'); + if ($field_config) { + $field_config->delete(); + } + + // Remove from form display. + $form_display = EntityFormDisplay::load('node.' . $bundle . '.default'); + if ($form_display) { + $form_display->removeComponent('field_price'); + $form_display->save(); + } + + // Remove from view display. + $view_display = EntityViewDisplay::load('node.' . $bundle . '.default'); + if ($view_display) { + $view_display->removeComponent('field_price'); + $view_display->save(); + } + } + + // Check if field storage should be deleted. + $this->cleanupFieldStorage(); + } + + /** + * Clean up field storage if no content types are using it. + */ + protected function cleanupFieldStorage() { + $field_storage = FieldStorageConfig::loadByName('node', 'field_price'); + if (!$field_storage) { + return; + } + + // Get all field configs that use this storage. + $field_configs = $this->entityTypeManager + ->getStorage('field_config') + ->loadByProperties([ + 'field_storage' => $field_storage, + ]); + + // If no field configs exist, delete the storage. + if (empty($field_configs)) { + $field_storage->delete(); + } + } + + /** + * Display messages about field changes. + * + * @param array $types_added + * Content types that had fields added. + * @param array $types_removed + * Content types that had fields removed. + */ + protected function displayFieldChanges(array $types_added, array $types_removed) { + $content_types = $this->entityTypeManager->getStorage('node_type')->loadMultiple(); + + if (!empty($types_added)) { + $added_labels = []; + foreach ($types_added as $type) { + if (isset($content_types[$type])) { + $added_labels[] = $content_types[$type]->label(); + } + } + if (!empty($added_labels)) { + $this->messenger()->addStatus( + $this->t('Price field added to: @types', [ + '@types' => implode(', ', $added_labels), + ]) + ); + } + } + + if (!empty($types_removed)) { + $removed_labels = []; + foreach ($types_removed as $type) { + if (isset($content_types[$type])) { + $removed_labels[] = $content_types[$type]->label(); + } + } + if (!empty($removed_labels)) { + $this->messenger()->addStatus( + $this->t('Price field removed from: @types', [ + '@types' => implode(', ', $removed_labels), + ]) + ); + } + } + } + +} +\ No newline at end of file diff --git a/src/Service/TurnstileService.php b/src/Service/TurnstileService.php @@ -0,0 +1,241 @@ +<?php + +namespace Drupal\turnstile\Service; + +use Drupal\Core\Config\ConfigFactoryInterface; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\RequestException; +use Psr\Log\LoggerInterface; + +/** + * Service for handling paywall functionality. + */ +class Turnstile { + + /** + * The configuration factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The HTTP client. + * + * @var \GuzzleHttp\ClientInterface + */ + protected $httpClient; + + /** + * Constructs a Turnstile object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The configuration factory. + * @param \GuzzleHttp\ClientInterface $http_client + * The HTTP client. + */ + public function __construct(ConfigFactoryInterface $config_factory, ClientInterface $http_client) { + $this->configFactory = $config_factory; + $this->httpClient = $http_client; + } + + /** + * Verify payment status with the backend. + * + * @param string $node_id + * The node ID. + * @param string $user_id + * The user ID. + * + * @return bool + * TRUE if payment is verified, FALSE otherwise. + */ + public function verifyPayment($node_id, $user_id) { + $config = $this->configFactory->get('turnstile.settings'); + $backend_url = $config->get('payment_backend_url'); + $access_token = $config->get('access_token'); + + if (empty($backend_url) || empty($access_token)) { + return FALSE; + } + + try { + $response = $this->httpClient->post($backend_url . '/verify', [ + 'json' => [ + 'node_id' => $node_id, + 'user_id' => $user_id, + ], + 'headers' => [ + 'Authorization' => 'Bearer ' . $access_token, + 'Content-Type' => 'application/json', + ], + ]); + + $data = json_decode($response->getBody(), TRUE); + return isset($data['verified']) && $data['verified'] === TRUE; + } + catch (RequestException $e) { + // Log the error and return FALSE. + \Drupal::logger('turnstile')->error('Payment verification failed: @message', ['@message' => $e->getMessage()]); + return FALSE; + } + } + + /** + * Check if user has access to content. + * + * @param string $node_id + * The node ID. + * @param string $user_id + * The user ID. + * + * @return bool + * TRUE if user has access, FALSE otherwise. + */ + public function hasAccess($node_id, $user_id) { + // Start session if not already started. + if (session_status() == PHP_SESSION_NONE) { + session_start(); + } + + // Check if user is a subscriber. + if (isset($_SESSION['paywall_subscriber']) && $_SESSION['paywall_subscriber'] === TRUE) { + return TRUE; + } + + // Check session-based orders. + if (isset($_SESSION['paywall_orders'][$node_id])) { + $order = $_SESSION['paywall_orders'][$node_id]; + + // Check if order is paid and not refunded. + if (isset($order['paid']) && $order['paid'] === TRUE && + (!isset($order['refunded']) || $order['refunded'] === FALSE)) { + return TRUE; + } + + // Check order status with backend if not marked as paid. + if (!$order['paid']) { + $status = $this->checkOrderStatus($order['order_id']); + if ($status && $status['paid']) { + // Update session with new status. + $_SESSION['paywall_orders'][$node_id]['paid'] = TRUE; + return TRUE; + } + } + } + + // Fallback to backend verification. + return $this->verifyPayment($node_id, $user_id); + } + + /** + * Check order status with Taler backend. + * + * @param string $order_id + * The order ID to check. + * + * @return array|FALSE + * Order status information or FALSE on failure. + */ + public function checkOrderStatus($order_id) { + $config = $this->configFactory->get('turnstile.settings'); + $backend_url = $config->get('payment_backend_url'); + $access_token = $config->get('access_token'); + + if (empty($backend_url) || empty($access_token)) { + return FALSE; + } + + try { + $response = $this->httpClient->get($backend_url . '/private/orders/' . $order_id, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $access_token, + ], + ]); + + $result = json_decode($response->getBody(), TRUE); + + return [ + 'order_id' => $order_id, + 'paid' => isset($result['order_status']) && $result['order_status'] === 'paid', + 'refunded' => isset($result['refunded']) && $result['refunded'] === TRUE, + 'order_status' => $result['order_status'] ?? 'unknown', + ]; + } + catch (RequestException $e) { + \Drupal::logger('turnstile')->error('Failed to check order status: @message', ['@message' => $e->getMessage()]); + return FALSE; + } + } + + /** + * Create a new Taler order. + * + * @param \Drupal\node\NodeInterface $node + * The node to create an order for. + * + * @return array|FALSE + * Order information or FALSE on failure. + */ + public function createOrder($node) { + $config = $this->configFactory->get('turnstile.settings'); + $backend_url = $config->get('payment_backend_url'); + $access_token = $config->get('access_token'); + + if (empty($backend_url) || empty($access_token)) { + return FALSE; + } + + $price = $node->get('field_price')->value; + if (empty($price)) { + return FALSE; + } + + $fulfillment_url = $node->toUrl('canonical', ['absolute' => TRUE])->toString(); + $order_data = [ + 'order' => [ + 'amount' => $price, + 'summary' => 'Access to: ' . $node->getTitle(), + 'fulfillment_url' => $fulfillment_url, + 'products' => [ + [ + 'product_id' => 'node_' . $node->id(), + 'description' => 'Access to article: ' . $node->getTitle(), + 'quantity' => 1, + 'price' => $price, + ] + ], + ], + 'create_token' => TRUE, + ]; + + try { + $response = $this->httpClient->post($backend_url . '/private/orders', [ + 'json' => $order_data, + 'headers' => [ + 'Authorization' => 'Bearer ' . $access_token, + 'Content-Type' => 'application/json', + ], + ]); + + $result = json_decode($response->getBody(), TRUE); + + if (isset($result['order_id'])) { + return [ + 'order_id' => $result['order_id'], + 'order_token' => $result['token'] ?? '', + 'payment_url' => $backend_url . '/orders/' . $result['order_id'], + 'paid' => FALSE, + 'refunded' => FALSE, + 'created' => time(), + ]; + } + } + catch (RequestException $e) { + \Drupal::logger('turnstile')->error('Failed to create Taler order: @message', ['@message' => $e->getMessage()]); + } + + return FALSE; + } + +} +\ No newline at end of file diff --git a/turnstile.info.yml b/turnstile.info.yml @@ -0,0 +1,10 @@ +name: Turnstile +type: module +description: 'Adds price field to nodes and implements paywall functionality with configurable content transformation.' +core_version_requirement: ^9 || ^10 +package: System +dependencies: + - drupal:node + - drupal:field + - drupal:user +configure: turnstile.settings diff --git a/turnstile.install b/turnstile.install @@ -0,0 +1,81 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Turnstile module. + */ + +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\field\Entity\FieldConfig; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityViewDisplay; + +/** + * Implements hook_install(). + */ +function turnstile_install() { + // Create the price field storage. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_price', + 'entity_type' => 'node', + 'type' => 'string', + 'cardinality' => 1, + 'settings' => [ + 'max_length' => 255, + ], + ]); + $field_storage->save(); + + // Get enabled content types from config or default to 'article'. + $config = \Drupal::config('turnstile.settings'); + $enabled_types = $config->get('enabled_content_types') ?: ['article']; + + // Add the price field to enabled content types. + foreach ($enabled_types as $bundle) { + if (\Drupal::entityTypeManager()->getStorage('node_type')->load($bundle)) { + $field_config = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => $bundle, + 'label' => 'Price', + 'description' => 'Price for accessing this content', + 'required' => FALSE, + ]); + $field_config->save(); + + // Add to form display. + $form_display = EntityFormDisplay::load('node.' . $bundle . '.default'); + if ($form_display) { + $form_display->setComponent('field_price', [ + 'type' => 'string_textfield', + 'weight' => 10, + ]); + $form_display->save(); + } + + // Add to view display. + $view_display = EntityViewDisplay::load('node.' . $bundle . '.default'); + if ($view_display) { + $view_display->setComponent('field_price', [ + 'type' => 'string', + 'weight' => 10, + 'label' => 'above', + ]); + $view_display->save(); + } + } + } +} + +/** + * Implements hook_uninstall(). + */ +function turnstile_uninstall() { + // Remove field configurations. + $field_storage = FieldStorageConfig::loadByName('node', 'field_price'); + if ($field_storage) { + $field_storage->delete(); + } + + // Clean up configuration. + \Drupal::configFactory()->getEditable('turnstile.settings')->delete(); +} +\ No newline at end of file diff --git a/turnstile.links.menu.yml b/turnstile.links.menu.yml @@ -0,0 +1,6 @@ +turnstile.settings: + title: 'Turnstile' + description: 'Configure Turnstile settings' + parent: system.admin_config_system + route_name: turnstile.settings + weight: 99 diff --git a/turnstile.module b/turnstile.module @@ -0,0 +1,98 @@ +<?php + +/** + * @file + * Main module file for Turnstile. + */ + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\node\NodeInterface; + +/** + * Implements hook_entity_view_alter(). + */ +function turnstile_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + // Only process nodes. + if (!$entity instanceof NodeInterface) { + return; + } + + // Get module configuration. + $config = \Drupal::config('turnstile.settings'); + $enabled_types = $config->get('enabled_content_types') ?: []; + + // Check if this node type is enabled for paywall. + if (!in_array($entity->bundle(), $enabled_types)) { + return; + } + + // Check if the node has a price field and it's not empty. + if (!$entity->hasField('field_price') || $entity->get('field_price')->isEmpty()) { + return; + } + + // Get the original body content. + if ($entity->hasField('body') && !$entity->get('body')->isEmpty()) { + $body_value = $entity->get('body')->value; + + // Call the paywall check function. + $transformed_body = turnstile_check_paywall_cookie($body_value); + + // Update the body in the build array. + if (isset($build['body'][0]['#text'])) { + $build['body'][0]['#text'] = $transformed_body; + } + } +} + +/** + * Custom function to check paywall cookie and transform content. + * + * @param string $body + * The original body content. + * + * @return string + * The transformed body content. + */ +function turnstile_check_paywall_cookie($body) { + // Check if the payment cookie is set. + if (isset($_COOKIE['payment_cookie'])) { + // User has paid, return original content. + return $body; + } + + // User hasn't paid, transform content to lowercase. + return strtolower($body); +} + + +/** + * Implements hook_form_FORM_ID_alter() for node forms. + */ +function turnstile_form_node_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { + $node = $form_state->getFormObject()->getEntity(); + $config = \Drupal::config('turnstile.settings'); + $enabled_types = $config->get('enabled_content_types') ?: []; + + // Only show price field on enabled content types. + if (in_array($node->bundle(), $enabled_types)) { + if (isset($form['field_price'])) { + $form['field_price']['#group'] = 'meta'; + $form['field_price']['widget'][0]['value']['#description'] = t('Set a price to enable paywall protection for this content.'); + } + } +} + +/** + * Implements hook_theme(). + */ +function turnstile_theme() { + return [ + 'turnstile_settings' => [ + 'variables' => [ + 'config' => NULL, + ], + ], + ]; +} +\ No newline at end of file diff --git a/turnstile.permissions.yml b/turnstile.permissions.yml @@ -0,0 +1,4 @@ +administer turnstile: + title: 'Administer Turnstile' + description: 'Configure Turnstile settings and manage paywall-protected content.' + restrict access: true +\ No newline at end of file diff --git a/turnstile.routing.yml b/turnstile.routing.yml @@ -0,0 +1,9 @@ +turnstile.settings: + path: '/admin/config/system/Turnstile' + defaults: + _form: '\Drupal\turnstile\Form\TurnstileSettingsForm' + _title: 'Turnstile Settings' + requirements: + _permission: 'administer Turnstile' + options: + _admin_route: TRUE +\ No newline at end of file diff --git a/turnstile.services.yml b/turnstile.services.yml @@ -0,0 +1,4 @@ +services: + turnstile.turnstile: + class: Drupal\turnstile\Service\Turnstile + arguments: ['@config.factory', '@http_client']