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)