feat: card rail via LNbits fiat providers (Stripe), per operator
A guest may pay a fiat-priced room by card when its owner has opted in and
LNbits core has a fiat provider for that user — resolved through
settings.get_fiat_providers_for_user(owner), the seam lnbits#67's per-user
Stripe Connect credentials will plug into; chatelet stores no credentials.
Both rails now go through create_payment_request: Lightning unchanged
(sats, deposit_sat), card charges the same deposit share of the fiat price
in the room's currency with extra.checkout parameterising the hosted
Stripe page — success/cancel return to {frontend}/chatelet/{room}?checkout=…
&booking=<id>, customer_email, line item, metadata. frontend_url is
allow-listed against the instance's trusted origins (ported from events)
and resolved before the hold so a refused rail never leaves a dead hold.
Core settles the Stripe webhook onto the same invoice queue, so
tasks.on_invoice_paid confirms card bookings unchanged.
BookingRequestData gains payment_method / fiat_provider / frontend_url;
BookingQuote gains fiat_payment_request / fiat_provider / is_fiat and a
nullable payment_request. RPC chatelet_booking_request passes the fields
through. min_lnbits_version → 1.4.1 (events' floor for these APIs).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
8557ce617e
commit
0dad30b648
11 changed files with 417 additions and 36 deletions
47
frontend.py
Normal file
47
frontend.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"""Where to send a guest back to after a hosted (Stripe) checkout.
|
||||
|
||||
Ported from the events extension. The calling app names itself via
|
||||
`frontend_url`; we only honour origins the LNbits instance already trusts
|
||||
(the CORS allow-list, its own base URL, the configured custom frontend), and
|
||||
fail loud on anything else — a wrong root would strand the guest in the
|
||||
wrong app after paying. Transport-agnostic: the HTTP door passes the request
|
||||
base URL as fallback, the RPC door has none and falls back to the instance.
|
||||
"""
|
||||
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
|
||||
def origin(url: str | None) -> str | None:
|
||||
if not url:
|
||||
return None
|
||||
parts = urlsplit(url.strip())
|
||||
if not parts.scheme or not parts.netloc:
|
||||
return None
|
||||
return f"{parts.scheme.lower()}://{parts.netloc.lower()}"
|
||||
|
||||
|
||||
def allowed_frontend_origins() -> set[str]:
|
||||
origins: set[str] = set()
|
||||
for candidate in [
|
||||
*getattr(settings, "lnbits_cors_allowed_origins", []),
|
||||
settings.lnbits_baseurl,
|
||||
getattr(settings, "lnbits_custom_frontend_url", None),
|
||||
]:
|
||||
o = origin(candidate)
|
||||
if o:
|
||||
origins.add(o)
|
||||
return origins
|
||||
|
||||
|
||||
def resolve_frontend_root(
|
||||
frontend_url: str | None, fallback_base_url: str | None
|
||||
) -> str:
|
||||
"""Root under which `/chatelet/{room_id}` resolves for the guest."""
|
||||
if not frontend_url:
|
||||
return (fallback_base_url or settings.lnbits_baseurl or "").rstrip("/")
|
||||
o = origin(frontend_url)
|
||||
if not o or o not in allowed_frontend_origins():
|
||||
raise ValueError("frontend_url origin is not allowed.")
|
||||
return frontend_url.rstrip("/")
|
||||
Loading…
Add table
Add a link
Reference in a new issue