cleanup
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
@@ -68,6 +70,53 @@ extension_router = APIRouter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_wasm_extension_config(ext_id: str) -> dict:
|
||||||
|
candidate_dirs = [
|
||||||
|
Path(settings.lnbits_extensions_path, "extensions", ext_id),
|
||||||
|
Path(settings.lnbits_path, "lnbits", "extensions", ext_id),
|
||||||
|
]
|
||||||
|
for base in candidate_dirs:
|
||||||
|
conf_path = base / "config.json"
|
||||||
|
if not conf_path.is_file():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with open(conf_path) as json_file:
|
||||||
|
config = json.load(json_file)
|
||||||
|
return config if isinstance(config, dict) else {}
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_wasm_permissions_saved(ext_id: str, user_ext: UserExtension) -> None:
|
||||||
|
if get_extension_type(ext_id) != "wasm":
|
||||||
|
return
|
||||||
|
config = _load_wasm_extension_config(ext_id)
|
||||||
|
required = [
|
||||||
|
permission.get("id")
|
||||||
|
for permission in config.get("permissions", [])
|
||||||
|
if isinstance(permission, dict) and permission.get("id")
|
||||||
|
]
|
||||||
|
granted = user_ext.extra.granted_permissions if user_ext.extra else []
|
||||||
|
missing = [
|
||||||
|
permission for permission in required if permission not in (granted or [])
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Save WASM permissions before enabling this extension.",
|
||||||
|
)
|
||||||
|
|
||||||
|
payment_tags = config.get("payment_tags", [])
|
||||||
|
granted_tags = user_ext.extra.granted_payment_tags if user_ext.extra else []
|
||||||
|
has_granted_payment_tag = any(tag in (granted_tags or []) for tag in payment_tags)
|
||||||
|
if payment_tags and not has_granted_payment_tag:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Select at least one WASM payment tag before enabling this extension.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@extension_router.post("", dependencies=[Depends(check_admin)])
|
@extension_router.post("", dependencies=[Depends(check_admin)])
|
||||||
async def api_install_extension(data: CreateExtension):
|
async def api_install_extension(data: CreateExtension):
|
||||||
release = await InstallableExtension.get_extension_release(
|
release = await InstallableExtension.get_extension_release(
|
||||||
@@ -198,6 +247,8 @@ async def api_enable_extension(
|
|||||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||||
await create_user_extension(user_ext)
|
await create_user_extension(user_ext)
|
||||||
|
|
||||||
|
_ensure_wasm_permissions_saved(ext_id, user_ext)
|
||||||
|
|
||||||
if account_id.is_admin_id or not ext.requires_payment:
|
if account_id.is_admin_id or not ext.requires_payment:
|
||||||
user_ext.active = True
|
user_ext.active = True
|
||||||
await update_user_extension(user_ext)
|
await update_user_extension(user_ext)
|
||||||
|
|||||||
@@ -262,39 +262,81 @@ window.PageExtensions = {
|
|||||||
extension.inProgress = false
|
extension.inProgress = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async enableExtensionForUser(extension) {
|
async loadWasmCapabilities(extension) {
|
||||||
if (extension.extensionType === 'wasm') {
|
const {data} = await LNbits.api.request(
|
||||||
const wasmHost = this.extensions.find(ext => ext.id === 'wasm')
|
'GET',
|
||||||
if (!wasmHost || !wasmHost.isInstalled || !wasmHost.isActive) {
|
`/wasm/api/v1/extensions/${extension.id}/capabilities`,
|
||||||
Quasar.Notify.create({
|
this.g.user.wallets[0].adminkey
|
||||||
type: 'warning',
|
)
|
||||||
message:
|
const capabilities = data || {}
|
||||||
'Enable the WASM! host extension before using this extension.'
|
extension.permissions = capabilities.permissions || []
|
||||||
})
|
extension.paymentTags = capabilities.payment_tags || []
|
||||||
return
|
extension.grantedPermissions = capabilities.granted_permissions || []
|
||||||
}
|
extension.grantedPaymentTags = capabilities.granted_payment_tags || []
|
||||||
if (extension.permissions && extension.permissions.length) {
|
extension._grantedPermissions = extension.grantedPermissions.slice()
|
||||||
if (!extension._grantedPermissions) {
|
extension._grantedPaymentTags = extension.grantedPaymentTags.slice()
|
||||||
Quasar.Notify.create({
|
return capabilities
|
||||||
type: 'warning',
|
},
|
||||||
message: 'Save permissions before enabling this extension.'
|
showWasmPermissionsDialog(extension, data = {}) {
|
||||||
})
|
this.permissionsDialog.extension = extension
|
||||||
return
|
this.permissionsDialog.checked = (
|
||||||
}
|
data.granted_permissions ||
|
||||||
}
|
extension._grantedPermissions ||
|
||||||
if (extension.paymentTags && extension.paymentTags.length) {
|
extension.grantedPermissions ||
|
||||||
if (
|
[]
|
||||||
!extension._grantedPaymentTags ||
|
).slice()
|
||||||
!extension._grantedPaymentTags.length
|
this.permissionsDialog.missing = data.missing_permissions || []
|
||||||
) {
|
this.permissionsDialog.tags = (
|
||||||
Quasar.Notify.create({
|
data.granted_payment_tags ||
|
||||||
type: 'warning',
|
extension._grantedPaymentTags ||
|
||||||
message: 'Select payment tags before enabling this extension.'
|
extension.grantedPaymentTags ||
|
||||||
})
|
[]
|
||||||
return
|
).slice()
|
||||||
}
|
this.permissionsDialog.tagOptions =
|
||||||
}
|
data.payment_tags || extension.paymentTags || []
|
||||||
|
this.permissionsDialog.show = true
|
||||||
|
},
|
||||||
|
async ensureWasmPermissionsReady(extension) {
|
||||||
|
if (extension.extensionType !== 'wasm') return true
|
||||||
|
const wasmHost = this.extensions.find(ext => ext.id === 'wasm')
|
||||||
|
if (!wasmHost || !wasmHost.isInstalled || !wasmHost.isActive) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message:
|
||||||
|
'Enable the WASM! host extension before using this extension.'
|
||||||
|
})
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
let data = {}
|
||||||
|
try {
|
||||||
|
data = await this.loadWasmCapabilities(extension)
|
||||||
|
} catch (err) {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const required = (data.permissions || []).map(p => p.id).filter(Boolean)
|
||||||
|
const missing = required.filter(
|
||||||
|
permission => !extension.grantedPermissions.includes(permission)
|
||||||
|
)
|
||||||
|
const tags = data.payment_tags || []
|
||||||
|
const missingTags =
|
||||||
|
tags.length &&
|
||||||
|
!tags.some(tag => extension.grantedPaymentTags.includes(tag))
|
||||||
|
if (!missing.length && !missingTags) return true
|
||||||
|
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Save WASM permissions before using this extension.'
|
||||||
|
})
|
||||||
|
this.showWasmPermissionsDialog(extension, data)
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async openExtension(extension) {
|
||||||
|
if (!(await this.ensureWasmPermissionsReady(extension))) return
|
||||||
|
window.location.href = `${extension.id}/`
|
||||||
|
},
|
||||||
|
async enableExtensionForUser(extension) {
|
||||||
|
if (!(await this.ensureWasmPermissionsReady(extension))) return
|
||||||
if (extension.isPaymentRequired) {
|
if (extension.isPaymentRequired) {
|
||||||
this.showPayToEnable(extension)
|
this.showPayToEnable(extension)
|
||||||
return
|
return
|
||||||
@@ -348,22 +390,6 @@ window.PageExtensions = {
|
|||||||
this.selectedExtension.payToEnable.showQRCode = false
|
this.selectedExtension.payToEnable.showQRCode = false
|
||||||
this.showPayToEnableDialog = true
|
this.showPayToEnableDialog = true
|
||||||
},
|
},
|
||||||
openPermissionsDialog(extension) {
|
|
||||||
this.permissionsDialog.extension = extension
|
|
||||||
this.permissionsDialog.checked = extension._grantedPermissions
|
|
||||||
? extension._grantedPermissions.slice()
|
|
||||||
: extension.grantedPermissions
|
|
||||||
? extension.grantedPermissions.slice()
|
|
||||||
: []
|
|
||||||
this.permissionsDialog.missing = []
|
|
||||||
this.permissionsDialog.tags = extension._grantedPaymentTags
|
|
||||||
? extension._grantedPaymentTags.slice()
|
|
||||||
: extension.grantedPaymentTags
|
|
||||||
? extension.grantedPaymentTags.slice()
|
|
||||||
: []
|
|
||||||
this.permissionsDialog.tagOptions = []
|
|
||||||
this.permissionsDialog.show = true
|
|
||||||
},
|
|
||||||
cancelPermissionsDialog() {
|
cancelPermissionsDialog() {
|
||||||
this.permissionsDialog.show = false
|
this.permissionsDialog.show = false
|
||||||
this.permissionsDialog.extension = null
|
this.permissionsDialog.extension = null
|
||||||
@@ -380,47 +406,12 @@ window.PageExtensions = {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.permissionsDialog.extension = extension
|
|
||||||
this.permissionsDialog.checked = extension._grantedPermissions
|
|
||||||
? extension._grantedPermissions.slice()
|
|
||||||
: extension.grantedPermissions
|
|
||||||
? extension.grantedPermissions.slice()
|
|
||||||
: []
|
|
||||||
this.permissionsDialog.missing = []
|
|
||||||
this.permissionsDialog.tags = extension._grantedPaymentTags
|
|
||||||
? extension._grantedPaymentTags.slice()
|
|
||||||
: extension.grantedPaymentTags
|
|
||||||
? extension.grantedPaymentTags.slice()
|
|
||||||
: []
|
|
||||||
this.permissionsDialog.tagOptions = []
|
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const data = await this.loadWasmCapabilities(extension)
|
||||||
'GET',
|
this.showWasmPermissionsDialog(extension, data)
|
||||||
`/wasm/api/v1/extensions/${extension.id}/capabilities`,
|
|
||||||
this.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
if (data && Array.isArray(data.permissions)) {
|
|
||||||
extension.permissions = data.permissions
|
|
||||||
}
|
|
||||||
if (data && Array.isArray(data.missing_permissions)) {
|
|
||||||
this.permissionsDialog.missing = data.missing_permissions
|
|
||||||
}
|
|
||||||
if (data && Array.isArray(data.payment_tags)) {
|
|
||||||
extension.paymentTags = data.payment_tags
|
|
||||||
this.permissionsDialog.tagOptions = data.payment_tags
|
|
||||||
}
|
|
||||||
if (data && Array.isArray(data.granted_permissions)) {
|
|
||||||
extension.grantedPermissions = data.granted_permissions
|
|
||||||
this.permissionsDialog.checked = data.granted_permissions.slice()
|
|
||||||
}
|
|
||||||
if (data && Array.isArray(data.granted_payment_tags)) {
|
|
||||||
extension.grantedPaymentTags = data.granted_payment_tags
|
|
||||||
this.permissionsDialog.tags = data.granted_payment_tags.slice()
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
}
|
}
|
||||||
this.permissionsDialog.show = true
|
|
||||||
},
|
},
|
||||||
async confirmPermissionsDialog() {
|
async confirmPermissionsDialog() {
|
||||||
const ext = this.permissionsDialog.extension
|
const ext = this.permissionsDialog.extension
|
||||||
|
|||||||
@@ -291,8 +291,7 @@
|
|||||||
"
|
"
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
type="a"
|
@click="openExtension(extension)"
|
||||||
:href="extension.id + '/'"
|
|
||||||
:label="$t('open')"
|
:label="$t('open')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|||||||
@@ -162,6 +162,39 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
|
|||||||
assert release_info["is_version_compatible"] is True
|
assert release_info["is_version_compatible"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_wasm_extension_enable_requires_saved_permissions(mocker):
|
||||||
|
user = await create_user_account(
|
||||||
|
Account(
|
||||||
|
id=uuid4().hex,
|
||||||
|
username=f"user_{uuid4().hex[:8]}",
|
||||||
|
email=f"user_{uuid4().hex[:8]}@lnbits.com",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ext_id = f"wasm_{uuid4().hex[:8]}"
|
||||||
|
await create_installed_extension(make_installable_extension(ext_id))
|
||||||
|
mocker.patch(
|
||||||
|
"lnbits.core.views.extension_api.get_valid_extensions",
|
||||||
|
mocker.AsyncMock(return_value=[Extension(code=ext_id, is_valid=True)]),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"lnbits.core.views.extension_api.get_extension_type",
|
||||||
|
return_value="wasm",
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"lnbits.core.views.extension_api._load_wasm_extension_config",
|
||||||
|
return_value={
|
||||||
|
"extension_type": "wasm",
|
||||||
|
"permissions": [{"id": "ext.db.read_write"}],
|
||||||
|
"payment_tags": ["paidtasks"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await api_enable_extension(ext_id, AccountId(id=user.id))
|
||||||
|
assert exc.value.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user):
|
async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user):
|
||||||
regular_user = await create_user_account(
|
regular_user = await create_user_account(
|
||||||
|
|||||||
Reference in New Issue
Block a user