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_user_exists,
)
from lnbits.settings import settings
from ..crud import (
create_user_extension,
delete_dbversion,
drop_extension_db,
get_db_version,
get_db_versions,
get_installed_extension,
get_installed_extensions,
get_user_extension,
@ -492,3 +494,82 @@ async def delete_extension_db(ext_id: str):
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Cannot delete data for extension '{ext_id}'",
) 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.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.extensions import get_valid_extensions
from lnbits.decorators import (
check_admin,
check_admin_ui,
@ -29,8 +27,6 @@ from lnbits.settings import settings
from ...utils.exchange_rates import allowed_currencies
from ..crud import (
create_wallet,
get_db_versions,
get_installed_extensions,
get_user,
get_wallet,
)
@ -122,97 +118,6 @@ async def robots():
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(
"/extensions/builder/preview/{ext_id}",
name="extensions builder",
@ -370,6 +275,7 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
@generic_router.get("/payments")
@generic_router.get("/wallets")
@generic_router.get("/account")
@generic_router.get("/extensions")
@generic_router.get("/users", dependencies=admin_ui_checks)
@generic_router.get("/audit", 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,
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_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

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',
name: 'Node',
@ -192,6 +183,11 @@ const routes = [
path: '/extensions/builder',
name: 'ExtensionsBuilder',
component: PageExtensionBuilder
},
{
path: '/extensions',
name: 'Extensions',
component: PageExtensions
}
]

View file

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

View file

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

View file

@ -1,4 +1,5 @@
{% include('pages/payments.vue') %} {% include('pages/node.vue') %} {%
include('pages/audit.vue') %} {% include('pages/wallets.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": [
"js/pages/extensions_builder.js",
"js/pages/extensions.js",
"js/pages/payments.js",
"js/pages/node.js",
"js/pages/node-public.js",