turnstile

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

README.md (9223B)


      1 # GNU Taler Turnstile
      2 
      3 A Drupal module that asks users to subscribe or pay using GNU Taler
      4 before granting access to nodes.
      5 
      6 Inspired by [Paivana](https://taler.net/) (DD 76), Turnstile uses
      7 *payment templates* in the merchant backend so that the paywall page
      8 itself is fully static and cacheable: no order is created when a
      9 visitor merely loads a paywalled article, and no PHP session is
     10 started for unauthenticated traffic. Orders only get created once a
     11 visitor's GNU Taler wallet actually picks the template up.
     12 
     13 
     14 ## Features
     15 
     16 - Adds a "price category" field to configurable content types
     17 - Static, cacheable paywall pages: bot traffic never creates orders
     18   in the merchant backend and never starts a PHP session
     19 - Access control without tracking — paid access is held in a
     20   short-lived HMAC cookie, no database table maps users to articles
     21 - Truncates node bodies to show teasers for visitors who did not (yet) pay
     22 - Server-side amount-check on every payment confirmation: the paid
     23   contract must match the price category configured for the
     24   fulfillment URL, otherwise the cookie is not minted
     25 - Configurable which content types are subject to access control
     26 - Subscription support with optional per-category discounts
     27 
     28 
     29 ## Installation
     30 
     31 1. Download and extract the module to your `modules/custom/` directory.
     32 2a. Enable the module via Drush: `drush en taler_turnstile`, or
     33 2b. Enable via the Drupal admin interface at `/admin/modules`.
     34 
     35 
     36 ## Configuration
     37 
     38 1. Navigate to `/admin/config/system/taler-turnstile` to configure:
     39 
     40 - **Enabled Content Types**: Select which content types should have
     41   the price field and access restriction.
     42 - **Payment Backend URL**: HTTP(S) URL of your Taler merchant backend
     43   (instance-specific URLs ending in `/instances/$ID/` are supported).
     44 - **Access Token**: Bearer token authenticating Turnstile to that
     45   merchant instance. The token must have the `templates-write` and
     46   `orders-read` permissions.
     47 - **Enable access if backend is down**: Fail-open toggle for the case
     48   where the merchant backend is not configured or unreachable.
     49 
     50 2. Make sure your Taler merchant backend is ready:
     51    - Bank account added
     52    - Legitimization with the payment service provider completed
     53 
     54 3. Configure one or more classes of subscriptions (optional). Navigate
     55    to `/admin/config/system/taler_turnstile/subscription-prices` and
     56    set the price at which each subscription token family can be
     57    purchased.
     58 
     59 The first time you save a working backend URL + token, Turnstile will
     60 push a `paivana`-style template for every existing price category
     61 into the merchant backend (template ID `turnstile-{category_id}`).
     62 You do not need to create those templates by hand.
     63 
     64 
     65 ## Usage
     66 
     67 1. Define one or more price categories under
     68    `/admin/structure/taler-turnstile-price-categories`:
     69    - Define a per-article price in each currency you accept.
     70    - Optionally define a discounted (or zero) price for each
     71      subscription class. A price of exactly `0` for a subscription
     72      marks the category as fully covered by that subscription —
     73      subscribers see the article for free.
     74 2. Create or edit a node of an enabled content type.
     75 3. Pick a price category in the new "Price category" field to gate
     76    the article behind a paywall.
     77 4. Earn money:
     78    - Visitors without a valid `taler_turnstile_paivana` cookie see
     79      the teaser plus a payment button.
     80    - Once their wallet has paid the merchant template, Turnstile
     81      mints the cookie and they see the full article.
     82 
     83 Editing a price category, adding/removing currencies, or changing the
     84 subscription prices automatically refreshes the matching templates in
     85 the merchant backend. Deleting a price category removes its template.
     86 
     87 
     88 ## How it Works
     89 
     90 The paywall is structured around three components:
     91 
     92 1. **Payment templates**, one per price category, kept in the
     93    merchant backend. Templates carry the v1 `choices` array that
     94    describes every accepted (currency, amount, optional subscription
     95    inputs/outputs) tuple. There is **no `website_regex`**: each node
     96    has its own fulfillment URL and templates are matched by category,
     97    not by URL pattern.
     98 
     99 2. **The paywall page** rendered by `hook_entity_view_alter()`. It
    100    replaces the full-mode build with a teaser plus a small
    101    `taler-turnstile-payment-container` block carrying the merchant
    102    URL, template ID, fulfillment URL, and a pre-built
    103    `taler://pay-template/...` URI. The same URI is also advertised
    104    as a `Paivana:` HTTP header for non-JS Taler clients. The page
    105    declares `cookies:taler_turnstile_paivana` as a cache context and
    106    `Vary: Cookie`, so it caches cleanly for everyone without the
    107    cookie (the dominant high-traffic case) while still serving the
    108    full content to visitors who do hold a cookie.
    109 
    110 3. **The confirmation endpoint** at `/taler-turnstile/paivana`. The
    111    client-side JavaScript:
    112    - Generates a 32-byte random `nonce` and computes
    113      `paivana_id = cur_time-base64url(SHA256(nonce || website || cur_time))`.
    114    - Renders `taler://pay-template/HOST/PATH/template_id?session_id={paivana_id}&fulfillment_url={website}`
    115      as a QR code and click-through link.
    116    - Long-polls `{merchant_backend}sessions/{paivana_id}?fulfillment_url=...`.
    117    - Once the merchant reports `paid`, POSTs `{order_id, nonce, cur_time, website}`
    118      back to `/taler-turnstile/paivana`. Turnstile reconstructs the
    119      paivana ID, looks up the order in the merchant backend, and
    120      **verifies that the paid `amount` matches one of the prices
    121      configured for the price category of the node behind `website`**.
    122      Only then does it mint the cookie and 303-redirect.
    123 
    124 Access for non-subscribers is held entirely in the cookie's HMAC
    125 (keyed on Drupal's per-site `private_key` and bound to client IP +
    126 fulfillment URL). Clearing cookies forgets paid access. There is no
    127 database table tracking who paid for what.
    128 
    129 If a contract bought a subscription token, the slug + expiration are
    130 also stored in the (now-started) PHP session so subsequent articles
    131 in the same subscription category short-circuit the paywall without
    132 needing a fresh payment.
    133 
    134 
    135 ## File Structure
    136 
    137 ```
    138 taler_turnstile/
    139 ├── config/
    140 │   ├── install/
    141 │   │   └── taler_turnstile.settings.yml - default configuration values
    142 │   └── schema/
    143 │       └── taler_turnstile.schema.yml - configuration schema (partial, without subscription prices)
    144 ├── js/
    145 │   ├── qrcode.min.js - QR code library from https://github.com/davidshimjs/qrcodejs
    146 │   └── payment-button.js - generates paivana_id, polls merchant /sessions, POSTs confirmation
    147 ├── src/
    148 │   ├── Controller/
    149 │   │   └── PaivanaController.php - handles POST /taler-turnstile/paivana, verifies amount, mints cookie
    150 │   ├── Entity/
    151 │   │   └── TurnstilePriceCategory.php - Price category config entity (also drives template lifecycle)
    152 │   ├── EventSubscriber/
    153 │   │   └── TurnstileConfigSubscriber.php - Reacts to config saves: field injection + template re-sync
    154 │   ├── Form/
    155 │   │   ├── PriceCategoryForm.php - Add/edit form handler
    156 │   │   ├── PriceCategoryDeleteForm.php - Delete confirmation form
    157 │   │   ├── SubscriptionPricesForm.php - Configure subscription prices
    158 │   │   └── TurnstileSettingsForm.php - Configure basics of Turnstile
    159 │   ├── PaivanaCookie.php - HMAC-SHA256 cookie service (keyed on Drupal's private_key)
    160 │   ├── PriceCategoryListBuilder.php - Admin list page builder
    161 │   ├── TalerMerchantApiService.php - Backend interaction: templates, order verification
    162 │   └── TurnstileFieldManager.php - Manages price-category field injection
    163 ├── templates/
    164 │   └── taler-turnstile-payment-button.html.twig - Paywall block markup + styles
    165 ├── taler_turnstile.libraries.yml - JS libraries and dependencies
    166 ├── taler_turnstile.info.yml - Module metadata and dependencies
    167 ├── taler_turnstile.install - Install/uninstall hooks
    168 ├── taler_turnstile.module - Hook implementations
    169 ├── taler_turnstile.permissions.yml - Permission definitions
    170 ├── taler_turnstile.routing.yml - Route definitions (admin pages + /taler-turnstile/paivana)
    171 ├── taler_turnstile.services.yml - Service container definitions
    172 ├── taler_turnstile.links.menu.yml - Menu link to Structure menu
    173 ├── taler_turnstile.links.action.yml - Action link for adding price categories
    174 └── README.md
    175 ```
    176 
    177 
    178 ## Requirements
    179 
    180 - Drupal 9 or 10
    181 - PHP 8.1 or higher (the module uses native enums)
    182 - A GNU Taler merchant backend supporting the v29 (or newer)
    183   `paivana` template type and the public `/sessions/$SESSION_ID`
    184   endpoint
    185 - The Drupal `path_alias` module (used by the confirmation endpoint
    186   to map fulfillment URLs back to nodes)
    187 
    188 
    189 ## License
    190 
    191 GPLv2-or-later, see COPYING for the full license terms.
    192 
    193 
    194 ## TODO
    195 
    196 - test subscriptions end-to-end through the new template flow
    197 - LATER: use order/template expiration from the merchant backend
    198   (with the v1.1 implementation) instead of hard-coding 1 day
    199 - LATER: rate-limit template instantiation on a per-IP basis as
    200   suggested by DD 76 (Solution B)