Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cddd000e3e | ||
|
|
c65d373f23 | ||
|
|
087e095bcf | ||
|
|
027a161494 | ||
|
|
83ef05696e | ||
|
|
42962308e6 | ||
|
|
783ee256a1 | ||
|
|
4e805fd00e | ||
|
|
f54824cc44 | ||
|
|
768d2cbe2a | ||
|
|
efd36d0221 | ||
|
|
4684939e13 | ||
|
|
193ee20da4 | ||
|
|
2fe7ab4f83 | ||
|
|
10c8ca4ca1 | ||
|
|
f079762b71 | ||
|
|
2e042d2597 | ||
|
|
49b57c9f0b | ||
|
|
656c6cac5b | ||
|
|
45e773b66f | ||
|
|
f692ac6f12 | ||
|
|
78202a027b | ||
|
|
28fa0906a6 | ||
|
|
91354f8be4 | ||
|
|
5f5d45e89f | ||
|
|
52bb125a25 | ||
|
|
2ca1ed897c | ||
|
|
631f4e8455 | ||
|
|
b22f88b264 | ||
|
|
c08bc02930 | ||
|
|
e020a7c5a0 | ||
|
|
d2e8914835 | ||
|
|
5e79ca35ab | ||
|
|
4690d178a2 | ||
|
|
c5825aa4c3 | ||
|
|
ebe0c4e3c3 | ||
|
|
383bdb6312 | ||
|
|
a947686d79 | ||
|
|
28e9d32b44 |
@@ -24,7 +24,7 @@ callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
|
|||||||
async def api_generic_webhook_handler(
|
async def api_generic_webhook_handler(
|
||||||
provider_name: str, request: Request
|
provider_name: str, request: Request
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
logger.info(f"Received callback from provider: '{provider_name}'.")
|
|
||||||
if provider_name.lower() == "stripe":
|
if provider_name.lower() == "stripe":
|
||||||
payload = await request.body()
|
payload = await request.body()
|
||||||
sig_header = request.headers.get("Stripe-Signature")
|
sig_header = request.headers.get("Stripe-Signature")
|
||||||
@@ -181,10 +181,8 @@ async def _get_stripe_subscription_payment_options(
|
|||||||
|
|
||||||
|
|
||||||
async def handle_paypal_event(event: dict):
|
async def handle_paypal_event(event: dict):
|
||||||
event_id = event.get("id", "")
|
|
||||||
event_type = event.get("event_type", "")
|
event_type = event.get("event_type", "")
|
||||||
resource = event.get("resource", {})
|
resource = event.get("resource", {})
|
||||||
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
|
|
||||||
|
|
||||||
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
|
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
|
||||||
payment_hash = _paypal_extract_payment_hash(resource)
|
payment_hash = _paypal_extract_payment_hash(resource)
|
||||||
@@ -198,17 +196,20 @@ async def handle_paypal_event(event: dict):
|
|||||||
await check_fiat_status(payment)
|
await check_fiat_status(payment)
|
||||||
return
|
return
|
||||||
|
|
||||||
if event_type in ("PAYMENT.SALE.COMPLETED"):
|
if event_type in (
|
||||||
|
"PAYMENT.SALE.COMPLETED",
|
||||||
|
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
|
||||||
|
):
|
||||||
await _handle_paypal_subscription_payment(resource)
|
await _handle_paypal_subscription_payment(resource)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
|
logger.info(f"Unhandled PayPal event type: '{event_type}'.")
|
||||||
|
|
||||||
|
|
||||||
async def _handle_paypal_subscription_payment(resource: dict):
|
async def _handle_paypal_subscription_payment(resource: dict):
|
||||||
amount_info = resource.get("amount") or {}
|
amount_info = resource.get("amount") or {}
|
||||||
currency = (amount_info.get("currency") or "").upper()
|
currency = (amount_info.get("currency_code") or "").upper()
|
||||||
total = amount_info.get("total")
|
total = amount_info.get("value")
|
||||||
if not currency or total is None:
|
if not currency or total is None:
|
||||||
raise ValueError("PayPal subscription event missing amount.")
|
raise ValueError("PayPal subscription event missing amount.")
|
||||||
|
|
||||||
@@ -216,14 +217,18 @@ async def _handle_paypal_subscription_payment(resource: dict):
|
|||||||
if not custom_id:
|
if not custom_id:
|
||||||
raise ValueError("PayPal subscription event missing custom metadata.")
|
raise ValueError("PayPal subscription event missing custom metadata.")
|
||||||
|
|
||||||
payment_options = _deserialize_paypal_metadata(custom_id)
|
try:
|
||||||
|
metadata = json.loads(custom_id)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
payment_options = FiatSubscriptionPaymentOptions(**metadata)
|
||||||
if not payment_options.wallet_id:
|
if not payment_options.wallet_id:
|
||||||
raise ValueError("PayPal subscription event missing wallet_id.")
|
raise ValueError("PayPal subscription event missing wallet_id.")
|
||||||
|
|
||||||
memo = payment_options.memo or ""
|
memo = payment_options.memo or ""
|
||||||
extra = {
|
extra = {
|
||||||
**(payment_options.extra or {}),
|
**(payment_options.extra or {}),
|
||||||
"subscription_request_id": resource.get("billing_agreement_id"),
|
|
||||||
"fiat_method": "subscription",
|
"fiat_method": "subscription",
|
||||||
"tag": payment_options.tag,
|
"tag": payment_options.tag,
|
||||||
"subscription": {
|
"subscription": {
|
||||||
@@ -254,29 +259,3 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
|||||||
if pu.get("custom_id"):
|
if pu.get("custom_id"):
|
||||||
return pu.get("custom_id")
|
return pu.get("custom_id")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _deserialize_paypal_metadata(custom_id: str) -> FiatSubscriptionPaymentOptions:
|
|
||||||
try:
|
|
||||||
meta = json.loads(custom_id)
|
|
||||||
wallet_id = meta[0] if len(meta) > 0 else None
|
|
||||||
tag = meta[1] if len(meta) > 1 else None
|
|
||||||
subscription_request_id = meta[2] if len(meta) > 2 else None
|
|
||||||
extra_link = meta[3] if len(meta) > 3 else None
|
|
||||||
memo = meta[4] if len(meta) > 4 else None
|
|
||||||
|
|
||||||
extra = {
|
|
||||||
"link": extra_link,
|
|
||||||
"subscription_request_id": subscription_request_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
return FiatSubscriptionPaymentOptions(
|
|
||||||
wallet_id=wallet_id,
|
|
||||||
tag=tag,
|
|
||||||
subscription_request_id=subscription_request_id,
|
|
||||||
extra=extra,
|
|
||||||
memo=memo,
|
|
||||||
)
|
|
||||||
except (json.JSONDecodeError, IndexError) as e:
|
|
||||||
logger.warning(f"Failed to deserialize PayPal metadata: {e}")
|
|
||||||
return FiatSubscriptionPaymentOptions()
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.core.models.wallets import WalletTypeInfo
|
from lnbits.core.models.wallets import WalletTypeInfo
|
||||||
@@ -31,11 +30,6 @@ async def create_subscription(
|
|||||||
data: CreateFiatSubscription,
|
data: CreateFiatSubscription,
|
||||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
key_type: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> FiatSubscriptionResponse:
|
) -> FiatSubscriptionResponse:
|
||||||
logger.debug(
|
|
||||||
f"Creating subscription with provider '{provider}. '"
|
|
||||||
f"Subscription ID '{data.subscription_id}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
fiat_provider = await get_fiat_provider(provider)
|
fiat_provider = await get_fiat_provider(provider)
|
||||||
if not fiat_provider:
|
if not fiat_provider:
|
||||||
raise HTTPException(404, "Fiat provider not found")
|
raise HTTPException(404, "Fiat provider not found")
|
||||||
@@ -53,12 +47,6 @@ async def create_subscription(
|
|||||||
subscription_response = await fiat_provider.create_subscription(
|
subscription_response = await fiat_provider.create_subscription(
|
||||||
data.subscription_id, data.quantity, data.payment_options
|
data.subscription_id, data.quantity, data.payment_options
|
||||||
)
|
)
|
||||||
if subscription_response.error_message:
|
|
||||||
logger.warning(
|
|
||||||
f"Failed to create subscription with provider '{provider}': "
|
|
||||||
f"{subscription_response.error_message}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return subscription_response
|
return subscription_response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+18
-63
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from typing import Any, Literal
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -24,8 +24,6 @@ from .base import (
|
|||||||
FiatSubscriptionResponse,
|
FiatSubscriptionResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
FiatMethod = Literal["checkout", "subscription"]
|
|
||||||
|
|
||||||
|
|
||||||
class PayPalCheckoutOptions(BaseModel):
|
class PayPalCheckoutOptions(BaseModel):
|
||||||
class Config:
|
class Config:
|
||||||
@@ -36,21 +34,11 @@ class PayPalCheckoutOptions(BaseModel):
|
|||||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class PayPalSubscriptionOptions(BaseModel):
|
|
||||||
class Config:
|
|
||||||
extra = "ignore"
|
|
||||||
|
|
||||||
checking_id: str | None = None
|
|
||||||
payment_request: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class PayPalCreateInvoiceOptions(BaseModel):
|
class PayPalCreateInvoiceOptions(BaseModel):
|
||||||
class Config:
|
class Config:
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
|
||||||
fiat_method: FiatMethod = "checkout"
|
|
||||||
checkout: PayPalCheckoutOptions | None = None
|
checkout: PayPalCheckoutOptions | None = None
|
||||||
subscription: PayPalSubscriptionOptions | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class PayPalWallet(FiatProvider):
|
class PayPalWallet(FiatProvider):
|
||||||
@@ -108,14 +96,6 @@ class PayPalWallet(FiatProvider):
|
|||||||
opts = self._parse_create_opts(extra or {})
|
opts = self._parse_create_opts(extra or {})
|
||||||
if opts is None:
|
if opts is None:
|
||||||
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
|
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
|
||||||
if opts.fiat_method == "subscription":
|
|
||||||
term = opts.subscription or PayPalSubscriptionOptions()
|
|
||||||
checking_id = term.checking_id or urlsafe_short_hash()
|
|
||||||
return FiatInvoiceResponse(
|
|
||||||
ok=True,
|
|
||||||
checking_id=f"subscription_{checking_id}",
|
|
||||||
payment_request=term.payment_request or "",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._ensure_access_token()
|
await self._ensure_access_token()
|
||||||
@@ -191,7 +171,6 @@ class PayPalWallet(FiatProvider):
|
|||||||
or "https://lnbits.com"
|
or "https://lnbits.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Creating PayPal subscription with ID '{subscription_id}'")
|
|
||||||
if not payment_options.subscription_request_id:
|
if not payment_options.subscription_request_id:
|
||||||
payment_options.subscription_request_id = urlsafe_short_hash()
|
payment_options.subscription_request_id = urlsafe_short_hash()
|
||||||
payment_options.extra = payment_options.extra or {}
|
payment_options.extra = payment_options.extra or {}
|
||||||
@@ -203,6 +182,7 @@ class PayPalWallet(FiatProvider):
|
|||||||
await self._ensure_access_token()
|
await self._ensure_access_token()
|
||||||
payload = {
|
payload = {
|
||||||
"plan_id": subscription_id,
|
"plan_id": subscription_id,
|
||||||
|
"quantity": str(quantity),
|
||||||
"custom_id": self._serialize_metadata(payment_options),
|
"custom_id": self._serialize_metadata(payment_options),
|
||||||
"application_context": {
|
"application_context": {
|
||||||
"return_url": success_url,
|
"return_url": success_url,
|
||||||
@@ -225,7 +205,7 @@ class PayPalWallet(FiatProvider):
|
|||||||
return FiatSubscriptionResponse(
|
return FiatSubscriptionResponse(
|
||||||
ok=True,
|
ok=True,
|
||||||
checkout_session_url=approval_url,
|
checkout_session_url=approval_url,
|
||||||
subscription_request_id=data.get("id"),
|
subscription_request_id=payment_options.subscription_request_id,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
@@ -239,10 +219,6 @@ class PayPalWallet(FiatProvider):
|
|||||||
correlation_id: str,
|
correlation_id: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> FiatSubscriptionResponse:
|
) -> FiatSubscriptionResponse:
|
||||||
logger.debug(
|
|
||||||
f"Cancelling PayPal subscription '{subscription_id}'. "
|
|
||||||
f"Correlation ID '{correlation_id}'."
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
await self._ensure_access_token()
|
await self._ensure_access_token()
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
@@ -264,20 +240,12 @@ class PayPalWallet(FiatProvider):
|
|||||||
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
try:
|
try:
|
||||||
await self._ensure_access_token()
|
await self._ensure_access_token()
|
||||||
paypal_id = self._normalize_paypal_id(checking_id)
|
order_id = self._normalize_paypal_id(checking_id)
|
||||||
if paypal_id.startswith("subscription_"):
|
r = await self.client.get(
|
||||||
capture_id = paypal_id.replace("subscription_", "", 1)
|
f"/v2/checkout/orders/{order_id}", headers=self._auth_headers()
|
||||||
r = await self.client.get(
|
)
|
||||||
f"v2/payments/captures/{capture_id}", headers=self._auth_headers()
|
r.raise_for_status()
|
||||||
)
|
return self._status_from_order(r.json())
|
||||||
r.raise_for_status()
|
|
||||||
return self._status_from_capture(r.json())
|
|
||||||
else:
|
|
||||||
r = await self.client.get(
|
|
||||||
f"/v2/checkout/orders/{paypal_id}", headers=self._auth_headers()
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
|
||||||
return self._status_from_order(r.json())
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(f"Error getting PayPal order status: {exc}")
|
logger.debug(f"Error getting PayPal order status: {exc}")
|
||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
@@ -302,14 +270,6 @@ class PayPalWallet(FiatProvider):
|
|||||||
return FiatPaymentFailedStatus()
|
return FiatPaymentFailedStatus()
|
||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
def _status_from_capture(self, order: dict[str, Any]) -> FiatPaymentStatus:
|
|
||||||
status = (order.get("status") or "").upper()
|
|
||||||
if status in ["COMPLETED"]:
|
|
||||||
return FiatPaymentSuccessStatus()
|
|
||||||
if status in ["DECLINED", "FAILED", "CANCELLED", "CANCELED"]:
|
|
||||||
return FiatPaymentFailedStatus()
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
|
|
||||||
def _normalize_paypal_id(self, checking_id: str) -> str:
|
def _normalize_paypal_id(self, checking_id: str) -> str:
|
||||||
return (
|
return (
|
||||||
checking_id.replace("fiat_paypal_", "", 1)
|
checking_id.replace("fiat_paypal_", "", 1)
|
||||||
@@ -320,20 +280,15 @@ class PayPalWallet(FiatProvider):
|
|||||||
def _serialize_metadata(
|
def _serialize_metadata(
|
||||||
self, payment_options: FiatSubscriptionPaymentOptions
|
self, payment_options: FiatSubscriptionPaymentOptions
|
||||||
) -> str:
|
) -> str:
|
||||||
meta = [
|
md = {
|
||||||
payment_options.wallet_id,
|
"wallet_id": payment_options.wallet_id,
|
||||||
payment_options.tag,
|
"memo": payment_options.memo,
|
||||||
payment_options.subscription_request_id,
|
"extra": payment_options.extra,
|
||||||
]
|
"tag": payment_options.tag,
|
||||||
if payment_options.extra:
|
"subscription_request_id": payment_options.subscription_request_id,
|
||||||
meta.append(payment_options.extra.get("link", None))
|
}
|
||||||
|
raw = json.dumps(md)
|
||||||
# Keep custom_id within PayPal's 127-char limit
|
return raw[:127] # PayPal custom_id limit
|
||||||
memo_limit = 120 - len(json.dumps(meta))
|
|
||||||
if memo_limit > 0 and payment_options.memo:
|
|
||||||
meta.append(payment_options.memo[:memo_limit])
|
|
||||||
|
|
||||||
return json.dumps(meta)
|
|
||||||
|
|
||||||
def _parse_create_opts(
|
def _parse_create_opts(
|
||||||
self, raw_opts: dict[str, Any]
|
self, raw_opts: dict[str, Any]
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -43,7 +43,52 @@ window.app.component('lnbits-qrcode', {
|
|||||||
nfcSupported: typeof NDEFReader != 'undefined'
|
nfcSupported: typeof NDEFReader != 'undefined'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
qrValue() {
|
||||||
|
return this.normalizeQrValue(this.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
normalizeQrValue(value) {
|
||||||
|
if (typeof value !== 'string' || value.length === 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = value.toLowerCase()
|
||||||
|
const upper = value.toUpperCase()
|
||||||
|
const isMixedCase = value !== lower && value !== upper
|
||||||
|
if (isMixedCase) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = 'lightning:'
|
||||||
|
if (lower.startsWith(prefix)) {
|
||||||
|
const prefixOriginal = value.slice(0, prefix.length)
|
||||||
|
const rest = value.slice(prefix.length)
|
||||||
|
const normalizedRest = this.normalizeBech32IfLower(rest)
|
||||||
|
if (normalizedRest !== rest) {
|
||||||
|
return prefixOriginal.toUpperCase() + normalizedRest
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.normalizeBech32IfLower(value)
|
||||||
|
},
|
||||||
|
normalizeBech32IfLower(value) {
|
||||||
|
if (typeof value !== 'string' || value.length === 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = value.toLowerCase()
|
||||||
|
if (value !== lower) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bech32 strings are case-insensitive, but must be all upper or all lower.
|
||||||
|
// Uppercasing enables QR alphanumeric mode and reduces QR size for LN strings.
|
||||||
|
const bech32Like = /^ln[0-9a-z]*1[0-9ac-hj-np-z]+$/
|
||||||
|
return bech32Like.test(value) ? value.toUpperCase() : value
|
||||||
|
},
|
||||||
printQrCode() {
|
printQrCode() {
|
||||||
const svg = this.$refs.qrCode.$el.outerHTML
|
const svg = this.$refs.qrCode.$el.outerHTML
|
||||||
const printWindow = window.open('', '_blank')
|
const printWindow = window.open('', '_blank')
|
||||||
|
|||||||
@@ -408,6 +408,7 @@
|
|||||||
<li><code>CHECKOUT.ORDER.APPROVED</code></li>
|
<li><code>CHECKOUT.ORDER.APPROVED</code></li>
|
||||||
<li><code>PAYMENT.CAPTURE.COMPLETED</code></li>
|
<li><code>PAYMENT.CAPTURE.COMPLETED</code></li>
|
||||||
<li><code>PAYMENT.SALE.COMPLETED</code></li>
|
<li><code>PAYMENT.SALE.COMPLETED</code></li>
|
||||||
|
<li><code>BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
>
|
>
|
||||||
<qrcode-vue
|
<qrcode-vue
|
||||||
ref="qrCode"
|
ref="qrCode"
|
||||||
:value="value"
|
:value="qrValue"
|
||||||
:margin="margin"
|
:margin="margin"
|
||||||
:size="size"
|
:size="size"
|
||||||
level="Q"
|
level="Q"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.0"
|
version = "1.5.0-rc1"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||||
|
|||||||
@@ -708,7 +708,7 @@ async def test_user_activation(
|
|||||||
f"/users/api/v1/user/{user_id}/activate",
|
f"/users/api/v1/user/{user_id}/activate",
|
||||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||||
)
|
)
|
||||||
|
print("### response", response.text)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json().get("message") == "User activated."
|
assert response.json().get("message") == "User activated."
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user