feat: move '/extensions' into vue component (#3480)

This commit is contained in:
dni ⚡ 2025-11-10 10:20:18 +01:00 committed by GitHub
parent c2a3fbc6c0
commit 0d5661cda7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1225 additions and 1166 deletions

File diff suppressed because it is too large Load diff

View file

@ -36,12 +36,14 @@ from lnbits.decorators import (
check_admin, check_admin,
check_user_exists, check_user_exists,
) )
from lnbits.settings import settings
from ..crud import ( from ..crud import (
create_user_extension, create_user_extension,
delete_dbversion, delete_dbversion,
drop_extension_db, drop_extension_db,
get_db_version, get_db_version,
get_db_versions,
get_installed_extension, get_installed_extension,
get_installed_extensions, get_installed_extensions,
get_user_extension, get_user_extension,
@ -492,3 +494,82 @@ async def delete_extension_db(ext_id: str):
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Cannot delete data for extension '{ext_id}'", detail=f"Cannot delete data for extension '{ext_id}'",
) from exc ) from exc
# TODO: create a response model for this
@extension_router.get("/all")
async def extensions(user: User = Depends(check_user_exists)):
installed_exts: list[InstallableExtension] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts]
installable_exts = await InstallableExtension.get_installable_extensions()
installable_exts_ids = [e.id for e in installable_exts]
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
for e in installable_exts:
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
if installed_ext and installed_ext.meta:
installed_release = installed_ext.meta.installed_release
if installed_ext.meta.pay_to_enable and not user.admin:
# not a security leak, but better not to share the wallet id
installed_ext.meta.pay_to_enable.wallet = None
pay_to_enable = installed_ext.meta.pay_to_enable
if e.meta:
e.meta.installed_release = installed_release
e.meta.pay_to_enable = pay_to_enable
else:
e.meta = ExtensionMeta(
installed_release=installed_release,
pay_to_enable=pay_to_enable,
)
# use the installed extension values
e.name = installed_ext.name
e.short_description = installed_ext.short_description
e.icon = installed_ext.icon
all_ext_ids = [ext.code for ext in await get_valid_extensions()]
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
db_versions = await get_db_versions()
extension_data = [
{
"id": ext.id,
"name": ext.name,
"icon": ext.icon,
"shortDescription": ext.short_description,
"stars": ext.stars,
"isFeatured": ext.meta.featured if ext.meta else False,
"dependencies": ext.meta.dependencies if ext.meta else "",
"isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": next(
(True for version in db_versions if version.db == ext.id), False
),
"isAvailable": ext.id in all_ext_ids,
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
"isActive": ext.id not in inactive_extensions,
"latestRelease": (
dict(ext.meta.latest_release)
if ext.meta and ext.meta.latest_release
else None
),
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
"paidFeatures": ext.meta.paid_features if ext.meta else False,
"installedRelease": (
dict(ext.meta.installed_release)
if ext.meta and ext.meta.installed_release
else None
),
"payToEnable": (
dict(ext.meta.pay_to_enable)
if ext.meta and ext.meta.pay_to_enable
else {}
),
"isPaymentRequired": ext.requires_payment,
"inProgress": False,
"selectedForUpdate": False,
}
for ext in installable_exts
]
return extension_data

View file

@ -14,9 +14,7 @@ from pydantic.types import UUID4
from lnbits.core.helpers import to_valid_user_id from lnbits.core.helpers import to_valid_user_id
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.core.models.extensions import ExtensionMeta, InstallableExtension
from lnbits.core.services import create_invoice, create_user_account from lnbits.core.services import create_invoice, create_user_account
from lnbits.core.services.extensions import get_valid_extensions
from lnbits.decorators import ( from lnbits.decorators import (
check_admin, check_admin,
check_admin_ui, check_admin_ui,
@ -29,8 +27,6 @@ from lnbits.settings import settings
from ...utils.exchange_rates import allowed_currencies from ...utils.exchange_rates import allowed_currencies
from ..crud import ( from ..crud import (
create_wallet, create_wallet,
get_db_versions,
get_installed_extensions,
get_user, get_user,
get_wallet, get_wallet,
) )
@ -122,97 +118,6 @@ async def robots():
return HTMLResponse(content=data, media_type="text/plain") return HTMLResponse(content=data, media_type="text/plain")
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
async def extensions(request: Request, user: User = Depends(check_user_exists)):
installed_exts: list[InstallableExtension] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts]
installable_exts = await InstallableExtension.get_installable_extensions()
installable_exts_ids = [e.id for e in installable_exts]
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
for e in installable_exts:
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
if installed_ext and installed_ext.meta:
installed_release = installed_ext.meta.installed_release
if installed_ext.meta.pay_to_enable and not user.admin:
# not a security leak, but better not to share the wallet id
installed_ext.meta.pay_to_enable.wallet = None
pay_to_enable = installed_ext.meta.pay_to_enable
if e.meta:
e.meta.installed_release = installed_release
e.meta.pay_to_enable = pay_to_enable
else:
e.meta = ExtensionMeta(
installed_release=installed_release,
pay_to_enable=pay_to_enable,
)
# use the installed extension values
e.name = installed_ext.name
e.short_description = installed_ext.short_description
e.icon = installed_ext.icon
all_ext_ids = [ext.code for ext in await get_valid_extensions()]
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
db_versions = await get_db_versions()
extension_data = [
{
"id": ext.id,
"name": ext.name,
"icon": ext.icon,
"shortDescription": ext.short_description,
"stars": ext.stars,
"isFeatured": ext.meta.featured if ext.meta else False,
"dependencies": ext.meta.dependencies if ext.meta else "",
"isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": next(
(True for version in db_versions if version.db == ext.id), False
),
"isAvailable": ext.id in all_ext_ids,
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
"isActive": ext.id not in inactive_extensions,
"latestRelease": (
dict(ext.meta.latest_release)
if ext.meta and ext.meta.latest_release
else None
),
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
"paidFeatures": ext.meta.paid_features if ext.meta else False,
"installedRelease": (
dict(ext.meta.installed_release)
if ext.meta and ext.meta.installed_release
else None
),
"payToEnable": (
dict(ext.meta.pay_to_enable)
if ext.meta and ext.meta.pay_to_enable
else {}
),
"isPaymentRequired": ext.requires_payment,
}
for ext in installable_exts
]
# refresh user state. Eg: enabled extensions.
# TODO: refactor
# user = await get_user(user.id) or user
return template_renderer().TemplateResponse(
request,
"core/extensions.html",
{
"user": user.json(),
"extension_data": extension_data,
"extension_builder_enabled": user.admin
or settings.lnbits_extensions_builder_activate_non_admins,
"ajax": _is_ajax_request(request),
},
)
@generic_router.get( @generic_router.get(
"/extensions/builder/preview/{ext_id}", "/extensions/builder/preview/{ext_id}",
name="extensions builder", name="extensions builder",
@ -370,6 +275,7 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
@generic_router.get("/payments") @generic_router.get("/payments")
@generic_router.get("/wallets") @generic_router.get("/wallets")
@generic_router.get("/account") @generic_router.get("/account")
@generic_router.get("/extensions")
@generic_router.get("/users", dependencies=admin_ui_checks) @generic_router.get("/users", dependencies=admin_ui_checks)
@generic_router.get("/audit", dependencies=admin_ui_checks) @generic_router.get("/audit", dependencies=admin_ui_checks)
@generic_router.get("/node", dependencies=admin_ui_checks) @generic_router.get("/node", dependencies=admin_ui_checks)

View file

@ -108,6 +108,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
"has_holdinvoice": settings.has_holdinvoice, "has_holdinvoice": settings.has_holdinvoice,
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(), "LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(), "LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
} }
t.env.globals["WINDOW_SETTINGS"] = window_settings t.env.globals["WINDOW_SETTINGS"] = window_settings

File diff suppressed because one or more lines are too long

View file

@ -139,15 +139,6 @@ const routes = [
} }
} }
}, },
{
path: '/extensions',
name: 'Extensions',
component: DynamicComponent,
props: {
fetchUrl: '/extensions',
scripts: ['/static/js/extensions.js']
}
},
{ {
path: '/node', path: '/node',
name: 'Node', name: 'Node',
@ -192,6 +183,11 @@ const routes = [
path: '/extensions/builder', path: '/extensions/builder',
name: 'ExtensionsBuilder', name: 'ExtensionsBuilder',
component: PageExtensionBuilder component: PageExtensionBuilder
},
{
path: '/extensions',
name: 'Extensions',
component: PageExtensions
} }
] ]

View file

@ -1,6 +1,9 @@
window.ExtensionsPageLogic = { window.PageExtensions = {
data: function () { template: '#page-extensions',
mixins: [windowMixin],
data() {
return { return {
extbuilderEnabled: false,
slide: 0, slide: 0,
fullscreen: false, fullscreen: false,
autoplay: true, autoplay: true,
@ -33,10 +36,10 @@ window.ExtensionsPageLogic = {
} }
}, },
methods: { methods: {
handleTabChanged: function (tab) { handleTabChanged(tab) {
this.filterExtensions(this.searchTerm, tab) this.filterExtensions(this.searchTerm, tab)
}, },
filterExtensions: function (term, tab) { filterExtensions(term, tab) {
// Filter the extensions list // Filter the extensions list
function extensionNameContains(searchTerm) { function extensionNameContains(searchTerm) {
return function (extension) { return function (extension) {
@ -65,7 +68,7 @@ window.ExtensionsPageLogic = {
this.tab = tab this.tab = tab
}, },
installExtension: async function (release) { async installExtension(release) {
// no longer required to check if the invoice was paid // no longer required to check if the invoice was paid
// the install logic has been triggered one way or another // the install logic has been triggered one way or another
this.unsubscribeFromPaylinkWs() this.unsubscribeFromPaylinkWs()
@ -101,7 +104,7 @@ window.ExtensionsPageLogic = {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
}, },
uninstallExtension: async function () { async uninstallExtension() {
const extension = this.selectedExtension const extension = this.selectedExtension
this.showManageExtensionDialog = false this.showManageExtensionDialog = false
this.showUninstallDialog = false this.showUninstallDialog = false
@ -137,7 +140,7 @@ window.ExtensionsPageLogic = {
extension.inProgress = false extension.inProgress = false
}) })
}, },
dropExtensionDb: async function () { async dropExtensionDb() {
const extension = this.selectedExtension const extension = this.selectedExtension
this.showManageExtensionDialog = false this.showManageExtensionDialog = false
this.showDropDbDialog = false this.showDropDbDialog = false
@ -187,14 +190,14 @@ window.ExtensionsPageLogic = {
extension.inProgress = false extension.inProgress = false
}) })
}, },
enableExtensionForUser: function (extension) { async enableExtensionForUser(extension) {
if (extension.isPaymentRequired) { if (extension.isPaymentRequired) {
this.showPayToEnable(extension) this.showPayToEnable(extension)
return return
} }
this.enableExtension(extension) this.enableExtension(extension)
}, },
enableExtension: function (extension) { async enableExtension(extension) {
LNbits.api LNbits.api
.request( .request(
'PUT', 'PUT',
@ -215,7 +218,7 @@ window.ExtensionsPageLogic = {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
}, },
disableExtension: function (extension) { disableExtension(extension) {
LNbits.api LNbits.api
.request( .request(
'PUT', 'PUT',
@ -236,14 +239,14 @@ window.ExtensionsPageLogic = {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
}, },
showPayToEnable: function (extension) { showPayToEnable(extension) {
this.selectedExtension = extension this.selectedExtension = extension
this.selectedExtension.payToEnable.paidAmount = this.selectedExtension.payToEnable.paidAmount =
extension.payToEnable.amount extension.payToEnable.amount
this.selectedExtension.payToEnable.showQRCode = false this.selectedExtension.payToEnable.showQRCode = false
this.showPayToEnableDialog = true this.showPayToEnableDialog = true
}, },
updatePayToInstallData: function (extension) { updatePayToInstallData(extension) {
LNbits.api LNbits.api
.request( .request(
'PUT', 'PUT',
@ -268,17 +271,17 @@ window.ExtensionsPageLogic = {
}) })
}, },
showUninstall: function () { showUninstall() {
this.showManageExtensionDialog = false this.showManageExtensionDialog = false
this.showUninstallDialog = true this.showUninstallDialog = true
this.uninstallAndDropDb = false this.uninstallAndDropDb = false
}, },
showDropDb: function () { showDropDb() {
this.showDropDbDialog = true this.showDropDbDialog = true
}, },
showManageExtension: async function (extension) { async showManageExtension(extension) {
this.selectedExtension = extension this.selectedExtension = extension
this.selectedRelease = null this.selectedRelease = null
this.selectedExtensionRepos = null this.selectedExtensionRepos = null
@ -323,7 +326,7 @@ window.ExtensionsPageLogic = {
} }
}, },
showExtensionDetails: async function (extId, detailsLink) { async showExtensionDetails(extId, detailsLink) {
if (!detailsLink) { if (!detailsLink) {
return return
} }
@ -528,14 +531,14 @@ window.ExtensionsPageLogic = {
} }
}, },
hasNewVersion: function (extension) { hasNewVersion(extension) {
if (extension.installedRelease && extension.latestRelease) { if (extension.installedRelease && extension.latestRelease) {
return ( return (
extension.installedRelease.version !== extension.latestRelease.version extension.installedRelease.version !== extension.latestRelease.version
) )
} }
}, },
isInstalledVersion: function (extension, release) { isInstalledVersion(extension, release) {
if (extension.installedRelease) { if (extension.installedRelease) {
return ( return (
extension.installedRelease.source_repo === release.source_repo && extension.installedRelease.source_repo === release.source_repo &&
@ -543,19 +546,19 @@ window.ExtensionsPageLogic = {
) )
} }
}, },
getReleaseIcon: function (release) { getReleaseIcon(release) {
if (!release.is_version_compatible) return 'block' if (!release.is_version_compatible) return 'block'
if (release.isInstalled) return 'download_done' if (release.isInstalled) return 'download_done'
return 'download' return 'download'
}, },
getReleaseIconColor: function (release) { getReleaseIconColor(release) {
if (!release.is_version_compatible) return 'text-red' if (!release.is_version_compatible) return 'text-red'
if (release.isInstalled) return 'text-green' if (release.isInstalled) return 'text-green'
return '' return ''
}, },
getGitHubReleaseDetails: async function (release) { async getGitHubReleaseDetails(release) {
if (!release.is_github_release || release.loaded) { if (!release.is_github_release || release.loaded) {
return return
} }
@ -579,10 +582,10 @@ window.ExtensionsPageLogic = {
release.inProgress = false release.inProgress = false
} }
}, },
selectAllUpdatableExtensionss: async function () { async selectAllUpdatableExtensionss() {
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true)) this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
}, },
updateSelectedExtensions: async function () { async updateSelectedExtensions() {
let count = 0 let count = 0
for (const ext of this.updatableExtensions) { for (const ext of this.updatableExtensions) {
try { try {
@ -628,14 +631,21 @@ window.ExtensionsPageLogic = {
setTimeout(() => { setTimeout(() => {
this.refreshRoute() this.refreshRoute()
}, 2000) }, 2000)
},
async fetchAllExtensions() {
try {
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
return data
} catch (error) {
console.warn(error)
LNbits.utils.notifyApiError(error)
return []
}
} }
}, },
created: function () { async created() {
this.extensions = window.extension_data.map(e => ({ this.extensions = await this.fetchAllExtensions()
...e, this.extbuilderEnabled = user.admin || this.LNBITS_EXT_BUILDER
inProgress: false,
selectedForUpdate: false
}))
this.filteredExtensions = this.extensions.concat([]) this.filteredExtensions = this.extensions.concat([])
const hash = window.location.hash.replace('#', '') const hash = window.location.hash.replace('#', '')
const ext = this.filteredExtensions.find(ext => ext.id === hash) const ext = this.filteredExtensions.find(ext => ext.id === hash)
@ -651,6 +661,5 @@ window.ExtensionsPageLogic = {
this.updatableExtensions = this.extensions.filter(ext => this.updatableExtensions = this.extensions.filter(ext =>
this.hasNewVersion(ext) this.hasNewVersion(ext)
) )
}, }
mixins: [windowMixin]
} }

View file

@ -44,6 +44,7 @@
], ],
"components": [ "components": [
"js/pages/extensions_builder.js", "js/pages/extensions_builder.js",
"js/pages/extensions.js",
"js/pages/payments.js", "js/pages/payments.js",
"js/pages/node.js", "js/pages/node.js",
"js/pages/node-public.js", "js/pages/node-public.js",

View file

@ -1,4 +1,5 @@
{% include('pages/payments.vue') %} {% include('pages/node.vue') %} {% {% include('pages/payments.vue') %} {% include('pages/node.vue') %} {%
include('pages/audit.vue') %} {% include('pages/wallets.vue') %} {% include('pages/audit.vue') %} {% include('pages/wallets.vue') %} {%
include('pages/users.vue') %} {% include('pages/admin.vue') %} {% include('pages/users.vue') %} {% include('pages/admin.vue') %} {%
include('pages/account.vue') %} {% include('pages/extensions_builder.vue') %} include('pages/account.vue') %} {% include('pages/extensions_builder.vue') %} {%
include('pages/extensions.vue') %}

File diff suppressed because it is too large Load diff

View file

@ -96,6 +96,7 @@
], ],
"components": [ "components": [
"js/pages/extensions_builder.js", "js/pages/extensions_builder.js",
"js/pages/extensions.js",
"js/pages/payments.js", "js/pages/payments.js",
"js/pages/node.js", "js/pages/node.js",
"js/pages/node-public.js", "js/pages/node-public.js",