Levels
This commit is contained in:
@@ -8,15 +8,16 @@ Both payment methods feed the same LDAP group reconciliation and local expiry.
|
||||
|
||||
## Membership flow
|
||||
|
||||
1. Anyone can request a free account. A one-hour, single-use email link allows
|
||||
1. Anyone can request a free level-0 account. A one-hour, single-use email link allows
|
||||
them to choose a password. The account is created as an `inetOrgPerson` in
|
||||
OpenLDAP; the password goes only through RFC 3062 Password Modify over
|
||||
certificate-verified LDAPS/StartTLS by default (see the explicit plaintext
|
||||
option below). This app never stores a password/hash.
|
||||
2. Sign-in uses Dex Authorization Code + PKCE, state and nonce. Authlib verifies
|
||||
the RS256 ID token, issuer, audience, signature and lifetime. A signed
|
||||
`federated_claims` connector/user ID must match the configured LDAP connector
|
||||
and LDAP `entryUUID`; accounts are **not** linked using email alone.
|
||||
2. Sign-in uses the single Dex LDAP option with Authorization Code + PKCE, state
|
||||
and nonce. Authlib verifies the RS256 ID token, issuer, audience, signature
|
||||
and lifetime. The signed connector/UID must match the configured connector
|
||||
and LDAP identity; account ownership is anchored to immutable `entryUUID`,
|
||||
never email alone.
|
||||
3. Authenticated users choose a server-allowlisted recurring Stripe Price.
|
||||
Stripe hosts Checkout and the billing portal. Return URLs never grant access.
|
||||
4. Stripe webhooks are signature-verified and durably deduplicated. The worker
|
||||
@@ -45,13 +46,22 @@ The live Stripe dashboard was inspected on 2026-10-23. This integration reuses
|
||||
**Subscription**, product `prod_Sc9N5J0vNAdLCn`, without modifying its prices,
|
||||
metadata, Discourse integration, or existing subscriptions.
|
||||
|
||||
| Tier | Monthly USD | Existing live Price ID | Proposed dedicated LDAP group |
|
||||
| Membership | Monthly USD | Existing live Price ID | Proposed dedicated LDAP group |
|
||||
| --- | ---: | --- | --- |
|
||||
| Level 0 | $4 | `price_1Rgv4xC5KPXqTvVLfMd6z7wn` | `www-level-0` |
|
||||
| Level 1 | $8 | `price_1Rgv5QC5KPXqTvVLv1PaHWb5` | `www-level-1` |
|
||||
| Level 2 | $16 | `price_1Rgv5rC5KPXqTvVL8Ol8sMUf` | `www-level-2` |
|
||||
| Level 3 | $32 | `price_1Rgv6FC5KPXqTvVL39mAxYjm` | `www-level-3` |
|
||||
| Level 4 | $64 | `price_1Rgv6mC5KPXqTvVLkcmznWZa` | `www-level-4` |
|
||||
| Level 0 · Community | Free | — | — |
|
||||
| Level 1 · Supporter | $4 | `price_1Rgv4xC5KPXqTvVLfMd6z7wn` | `www-level-1` |
|
||||
| Level 2 · Insider | $8 | `price_1Rgv5QC5KPXqTvVLv1PaHWb5` | `www-level-2` |
|
||||
| Level 3 · Collaborator | $16 | `price_1Rgv5rC5KPXqTvVL8Ol8sMUf` | `www-level-3` |
|
||||
| Level 4 · Sustainer | $32 | `price_1Rgv6FC5KPXqTvVL39mAxYjm` | `www-level-4` |
|
||||
| Level 5 · Patron | $64 | `price_1Rgv6mC5KPXqTvVLkcmznWZa` | `www-level-5` |
|
||||
|
||||
Every account has a non-null `membership_level`: new/free accounts start at 0,
|
||||
and synchronization records the highest currently valid paid level. The former
|
||||
paid levels 0–4 are shifted to 1–5; Stripe Price IDs remain unchanged. The
|
||||
migration also updates stored subscription and prepaid-invoice tier keys and
|
||||
backfills current account levels. Existing level-0 LDAP group grants are
|
||||
revoked by the worker after the migration; retain that legacy group until the
|
||||
first successful synchronization of existing accounts.
|
||||
|
||||
The deployable mapping is in [membership/tiers.eom.json](membership/tiers.eom.json).
|
||||
The **group DNs are deployment proposals**, not verified existing groups: create
|
||||
@@ -97,17 +107,18 @@ of these five prices when inspected.
|
||||
and exposes bind passwords, registration passwords and directory data to the
|
||||
network. The default is `0` (StartTLS required for `ldap://`); TLS failures never
|
||||
trigger a plaintext fallback, and `ldaps://` always verifies certificates.
|
||||
4. Add a dedicated Dex client/LDAP connector using
|
||||
4. Add the website client to Dex and merge website account entries into its
|
||||
existing LDAP connector using
|
||||
[membership/deploy/dex-membership.example.yaml](membership/deploy/dex-membership.example.yaml).
|
||||
The exact callback is `https://eom.dev/members/oidc/callback/` (trailing slash).
|
||||
Set the same client secret in Dex and `DEX_CLIENT_SECRET`, and set
|
||||
`DEX_CONNECTOR_ID=www-ldap`. This connector uses `idAttr: entryUUID`,
|
||||
`preferredUsernameAttr: uid`, `emailAttr: mail`; scopes include `federated:id`.
|
||||
**Do not change the existing connector's idAttr blindly**: that would change
|
||||
subjects in other apps. Use the additional connector instead. Dex must search
|
||||
the account OU without requiring membership in a paid group. RS256 and all
|
||||
discovery endpoints must be HTTPS on the issuer's origin. Existing lowercase
|
||||
LDAP usernames matching the registration syntax can sign in without signup.
|
||||
`DEX_CONNECTOR_ID=ldap`. Keep the connector ID and `idAttr: uid` unchanged to
|
||||
preserve subjects in other apps. Add the website `inetOrgPerson` branch to the
|
||||
LDAP filter; this allows those entries to authenticate to all Dex clients, so
|
||||
review that scope. Dex uses signed `federated:id` claims, while the app resolves
|
||||
UID to LDAP and checks immutable `entryUUID`. RS256 and all discovery endpoints
|
||||
must be HTTPS on the issuer's origin. Existing lowercase LDAP usernames
|
||||
matching the registration syntax can sign in without signup.
|
||||
5. The supplied tier file contains live Price IDs. Set `STRIPE_LIVE_MODE=1` only
|
||||
with the matching live Stripe API key. For sandbox testing, override
|
||||
`MEMBERSHIP_TIERS_JSON` with **test-mode** recurring prices and set live mode
|
||||
@@ -193,8 +204,9 @@ To enable:
|
||||
or triggers an API request. The worker polls known local invoice UUIDs over
|
||||
authenticated HTTPS, so forged, repeated, missing or reordered callbacks
|
||||
cannot grant access or extend a term.
|
||||
5. Redeploy the image and run the migration service, which applies
|
||||
[membership/members/migrations/0002_crypto_invoices.py](membership/members/migrations/0002_crypto_invoices.py).
|
||||
5. Redeploy the image and run the migration service, which applies the pending
|
||||
membership schema/data migrations, including
|
||||
[membership/members/migrations/0003_membership_levels.py](membership/members/migrations/0003_membership_levels.py).
|
||||
Keep the membership worker running; it independently refreshes Stripe and
|
||||
SHKeeper, and performs a local-only expiry sweep before remote requests.
|
||||
6. Smoke-test a low-value invoice on your actual deployment before offering
|
||||
@@ -274,9 +286,9 @@ Payment lifecycle:
|
||||
current implementation follows paid invoices plus subscription state. If a
|
||||
refund/dispute should revoke access, cancel/suspend the subscription as part of
|
||||
your reviewed refund/dispute process.
|
||||
- Free accounts are `inetOrgPerson`, not automatically POSIX/shell accounts and
|
||||
not automatically members of any service group. Password resets remain your
|
||||
existing LDAP/Dex account recovery process.
|
||||
- Level-0 accounts are `inetOrgPerson`, not automatically POSIX/shell accounts
|
||||
and not automatically members of any service group. Password resets remain
|
||||
your existing LDAP/Dex account recovery process.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
Reference in New Issue
Block a user