Compare commits

..
Author SHA1 Message Date
Tiago Vasconcelos 9a9e638c1e fix: separate concerns 2026-03-19 11:13:32 +00:00
Tiago Vasconcelos 59a2fefbf3 chore: lint & bundle 2026-03-19 09:59:17 +00:00
Tiago Vasconcelos 4725eb95c9 users page dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos a5aaced420 node page dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 87a6514b62 some extensions dialogs
some need a bit more work
2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 32c893027d audit detail dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 751598e0f8 remove margin in title 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos e11ccd9757 account dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 7e344de10e funding sources qr 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 08fdeedffc exchange ticker dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 73ba9d94a5 wallet icon dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 1640e23f75 new wallet dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 9145c13435 qr scanner dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 41048b8cc7 icon prop takes real estate 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos b46e2c549d hodl invoice 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos b382ccfdf3 add optional secondary action button 2026-03-19 09:53:22 +00:00
Tiago Vasconcelos 1687972d89 payment details 2026-03-19 09:53:22 +00:00
Tiago Vasconcelos d621a6f81f . 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos b9d00d900e prettier 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos 0f06b7ba52 chore: bundle 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos 220ece6027 fix mixed bindings 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 1663845468 remove action slot 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 5b7d1e63b5 test fixes 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 5bd8bf2f56 init 2026-03-19 09:47:57 +00:00
28 changed files with 913 additions and 1086 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ RUN uv sync --all-extras
ENV LNBITS_PORT="5000" ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0" ENV LNBITS_HOST="0.0.0.0"
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet" ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
ENV FUNDING_SOURCE_MAX_RETRIES="10" ENV FUNDING_SOURCE_MAX_RETRIES=10
# Boltzd # Boltzd
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002" ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
+3 -5
View File
@@ -2,17 +2,15 @@ ARG LNBITS_TAG=latest
FROM lnbits/lnbits:${LNBITS_TAG} FROM lnbits/lnbits:${LNBITS_TAG}
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get -y upgrade RUN apt-get update && apt-get -y upgrade
RUN apt-get install -y ca-certificates curl gnupg netcat-openbsd git nodejs RUN apt-get install -y netcat-openbsd npm nodejs git
RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH" ENV PATH="/root/.local/bin:$PATH"
# install sparksidecar # install sparksidecar
RUN git clone https://github.com/lnbits/spark_sidecar RUN git clone https://github.com/lnbits/spark_sidecar
RUN cd spark_sidecar && npm ci RUN cd spark_sidecar && npm install
# Reinstall dependencies for lnbits just in case (needed for CMD usage) # Reinstall dependencies for lnbits just in case (needed for CMD usage)
RUN uv sync --all-extras RUN uv sync --all-extras
@@ -24,7 +22,7 @@ ENV LNBITS_BACKEND_WALLET_CLASS="SparkL2Wallet"
ENV LNBITS_RESERVE_FEE_MIN="20000" ENV LNBITS_RESERVE_FEE_MIN="20000"
ENV LNBITS_RESERVE_FEE_PERCENT="1" ENV LNBITS_RESERVE_FEE_PERCENT="1"
ENV LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS="20" ENV LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS="20"
ENV FUNDING_SOURCE_MAX_RETRIES="10" ENV FUNDING_SOURCE_MAX_RETRIES=10
# spark sidecar # spark sidecar
ENV SPARK_NETWORK="MAINNET" ENV SPARK_NETWORK="MAINNET"
+1 -2
View File
@@ -105,8 +105,7 @@ async def get_settings_field(
) )
if not row: if not row:
return None return None
value = json.loads(row["value"]) if row["value"] else None return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
return SettingsField(id=row["id"], value=value, tag=row["tag"])
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"): async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
-6
View File
@@ -173,12 +173,6 @@ async def check_admin_settings():
if account and account.extra and account.extra.provider == "env": if account and account.extra and account.extra.provider == "env":
settings.first_install = True settings.first_install = True
if settings.has_first_install_token_changed():
logger.warning("First install token is changed. Resetting admin settings.")
new_settings = await init_admin_settings()
settings.super_user = new_settings.super_user
settings.first_install = True
logger.success( logger.success(
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` " "✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
"to get the superuser." "to get the superuser."
-8
View File
@@ -12,7 +12,6 @@ from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID, SSOBase from fastapi_sso.sso.base import OpenID, SSOBase
from loguru import logger from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.crud.users import ( from lnbits.core.crud.users import (
get_user_access_control_lists, get_user_access_control_lists,
update_user_access_control_list, update_user_access_control_list,
@@ -533,13 +532,6 @@ async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
account.hash_password(data.password) account.hash_password(data.password)
await update_account(account) await update_account(account)
settings.first_install = False settings.first_install = False
# only confrm it after the super user has been successfully updated
if settings.first_install_token:
settings.first_install_token_confirmed = data.first_install_token
await set_settings_field(
"first_install_token_confirmed", data.first_install_token
)
return _auth_success_response(account.username, account.id, account.email) return _auth_success_response(account.username, account.id, account.email)
-8
View File
@@ -447,7 +447,6 @@ class SecuritySettings(LNbitsSettings):
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0) lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0) lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
first_install_token_confirmed: str | None = Field(default=None)
def is_wallet_max_balance_exceeded(self, amount): def is_wallet_max_balance_exceeded(self, amount):
return ( return (
@@ -1016,13 +1015,6 @@ class EnvSettings(LNbitsSettings):
def has_default_extension_path(self) -> bool: def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits" return self.lnbits_extensions_path == "lnbits"
def has_first_install_token_changed(self) -> bool:
if not self.first_install_token:
return False
if not settings.first_install_token_confirmed:
return False
return self.first_install_token != settings.first_install_token_confirmed
def check_auth_secret_key(self): def check_auth_secret_key(self):
if self.auth_secret_key: if self.auth_secret_key:
return return
File diff suppressed because one or more lines are too long
@@ -0,0 +1,105 @@
window.app.component('lnbits-dialog', {
template: '#lnbits-dialog',
props: {
show: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
position: {
type: String,
default: 'top'
},
persistent: {
type: Boolean,
default: false
},
showCancel: {
type: Boolean,
default: true
},
cancelLabel: {
type: String,
default: 'Close'
},
cancelColor: {
type: String,
default: 'grey'
},
action: {
type: Object,
default: null
},
secondaryAction: {
type: Object,
default: null
}
},
emits: ['update:show', 'hide', 'cancel', 'action', 'secondary-action'],
computed: {
hasAction() {
return !!(this.action && this.action.label)
},
hasSecondaryAction() {
return !!(this.secondaryAction && this.secondaryAction.label)
},
actionProps() {
const action = this.action || {}
const obj = {
label: action.label || 'Action',
color: action.color || 'primary',
loading: !!action.loading,
disable: !!action.disable,
closePopup: !!action.closePopup
}
if (action.icon) {
obj.icon = action.icon
}
return obj
},
secondaryActionProps() {
const action = this.secondaryAction || {}
const obj = {
label: action.label || 'Secondary',
color: action.color || 'secondary',
loading: !!action.loading,
disable: !!action.disable,
closePopup: !!action.closePopup
}
if (action.icon) {
obj.icon = action.icon
}
return obj
}
},
methods: {
handleModelUpdate(value) {
this.$emit('update:show', value)
},
handleHide() {
this.handleModelUpdate(false)
this.$emit('hide')
},
handleCancel() {
this.$emit('cancel')
},
handleAction() {
this.$emit('action')
},
handleSecondaryAction() {
this.$emit('secondary-action')
}
}
})
+1
View File
@@ -70,6 +70,7 @@
"js/components/admin/lnbits-admin-site-customisation.js", "js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-assets-config.js", "js/components/admin/lnbits-admin-assets-config.js",
"js/components/admin/lnbits-admin-audit.js", "js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-dialog.js",
"js/components/lnbits-wallet-charts.js", "js/components/lnbits-wallet-charts.js",
"js/components/lnbits-wallet-api-docs.js", "js/components/lnbits-wallet-api-docs.js",
"js/components/lnbits-wallet-icon.js", "js/components/lnbits-wallet-icon.js",
+2 -1
View File
@@ -30,7 +30,8 @@ include('components/lnbits-wallet-share.vue') %} {%
include('components/lnbits-wallet-charts.vue') %} {% include('components/lnbits-wallet-charts.vue') %} {%
include('components/lnbits-wallet-paylinks.vue') %} {% include('components/lnbits-wallet-paylinks.vue') %} {%
include('components/lnbits-wallet-extra.vue') %} {% include('components/lnbits-wallet-extra.vue') %} {%
include('components/lnbits-error.vue') %} include('components/lnbits-error.vue') %} {%
include('components/lnbits-dialog.vue') %}
<template id="lnbits-manage"> <template id="lnbits-manage">
<q-list v-if="g.user" dense class="lnbits-drawer__q-list"> <q-list v-if="g.user" dense class="lnbits-drawer__q-list">
@@ -199,12 +199,16 @@
</div> </div>
<div class="col-md-8 col-sm-12"></div> <div class="col-md-8 col-sm-12"></div>
</div> </div>
<lnbits-dialog
<q-dialog v-model="exchangeData.showTickerConversion" position="top"> :show="exchangeData.showTickerConversion"
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :title="$t('create_ticker_converter')"
<div class="q-mb-md"> :action="{
<strong v-text="$t('create_ticker_converter')"></strong> label: 'Add Ticker Conversion'
</div> }"
:cancel-label="$t('close')"
@update:show="exchangeData.showTickerConversion = $event"
@action="addExchangeTickerConversion()"
>
<div class="row"> <div class="row">
<div class="col-12 q-mb-md"> <div class="col-12 q-mb-md">
<q-select <q-select
@@ -226,20 +230,5 @@
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
@click="addExchangeTickerConversion()"
label="Add Ticker Conversion"
color="primary"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
@@ -125,15 +125,14 @@
</q-expansion-item> </q-expansion-item>
</div> </div>
</q-list> </q-list>
<q-dialog v-model="showQRDialog"> <lnbits-dialog
<q-card class="q-pa-md"> :show="showQRDialog"
:position="'standard'"
@update:show="showQRDialog = $event"
>
<q-card-section> <q-card-section>
<lnbits-qrcode :value="qrValue"></lnbits-qrcode> <lnbits-qrcode :value="qrValue"></lnbits-qrcode>
</q-card-section> </q-card-section>
<q-card-actions align="right"> </lnbits-dialog>
<q-btn flat :label="$t('close')" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div> </div>
</template> </template>
@@ -0,0 +1,61 @@
<template id="lnbits-dialog">
<q-dialog
:model-value="show"
:position="position"
:persistent="persistent"
@update:model-value="handleModelUpdate"
@hide="handleHide"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<!-- Header with optional title -->
<q-card-section v-if="title">
<div class="text-h6" v-text="title"></div>
</q-card-section>
<!-- Main content slot -->
<q-card-section>
<slot></slot>
</q-card-section>
<q-card-actions
v-if="showCancel || hasAction || hasSecondaryAction"
class="q-mt-lg"
align="right"
>
<q-btn
v-if="showCancel"
v-close-popup
flat
:color="cancelColor"
:label="cancelLabel"
@click="handleCancel"
></q-btn>
<q-space></q-space>
<q-btn
v-if="hasSecondaryAction"
:icon="secondaryActionProps.icon"
:color="secondaryActionProps.color"
:label="secondaryActionProps.label"
:loading="secondaryActionProps.loading"
:disable="secondaryActionProps.disable"
@click="handleSecondaryAction"
v-close-popup="secondaryActionProps.closePopup"
class="q-mr-sm"
></q-btn>
<q-btn
v-if="hasAction"
:icon="actionProps.icon"
:color="actionProps.color"
:label="actionProps.label"
:loading="actionProps.loading"
:disable="actionProps.disable"
@click="handleAction"
v-close-popup="actionProps.closePopup"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
</template>
@@ -394,9 +394,16 @@
></span> ></span>
</i> </i>
</q-td> </q-td>
<q-dialog v-model="props.expand" :props="props" position="top"> <lnbits-dialog
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card"> :show="props.expand"
<q-card-section> :cancel-label="$t('close')"
:action="{
label: $t('check_payment'),
icon: 'refresh'
}"
@update:show="props.expand = $event"
@action="checkPayment(props.row.payment_hash)"
>
<q-list bordered separator> <q-list bordered separator>
<q-expansion-item <q-expansion-item
expand-separator expand-separator
@@ -461,9 +468,7 @@
</q-list> </q-list>
<div <div
v-if=" v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
props.row.isIn && props.row.isPending && props.row.bolt11
"
> >
<div v-if="props.row.extra.fiat_payment_request"> <div v-if="props.row.extra.fiat_payment_request">
<lnbits-qrcode <lnbits-qrcode
@@ -479,27 +484,24 @@
></lnbits-qrcode> ></lnbits-qrcode>
</div> </div>
</div> </div>
<div class="row q-mt-md"> </lnbits-dialog>
<q-btn <lnbits-dialog
outline :show="hodlInvoice.show"
color="grey" :cancel-label="$t('close')"
@click="checkPayment(props.row.payment_hash)" :action="{
icon="refresh" label: $t('cancel_invoice'),
:label="$t('payment_check')" color: 'grey',
></q-btn> closePopup: true
<q-btn }"
v-close-popup :secondary-action="{
flat label: $t('settle_invoice'),
color="grey" color: 'grey',
class="q-ml-auto" closePopup: true
:label="$t('close')" }"
></q-btn> @update:show="hodlInvoice.show = $event"
</div> @action="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
</q-card-section> @secondary-action="settleHoldInvoice(hodlInvoice.preimage)"
</q-card> >
</q-dialog>
<q-dialog v-model="hodlInvoice.show" position="top">
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
<q-card-section> <q-card-section>
<q-item-label class="text-h6"> <q-item-label class="text-h6">
<span v-text="$t('hold_invoice')"></span> <span v-text="$t('hold_invoice')"></span>
@@ -520,33 +522,7 @@
> >
</q-input> </q-input>
</q-card-section> </q-card-section>
<q-card-section class="row q-gutter-x-sm"> </lnbits-dialog>
<q-btn
@click="settleHoldInvoice(hodlInvoice.preimage)"
outline
v-close-popup
color="grey"
:label="$t('settle_invoice')"
>
</q-btn>
<q-btn
v-close-popup
outline
color="grey"
class="q-ml-sm"
@click="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
:label="$t('cancel_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</q-card-section>
</q-card>
</q-dialog>
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
@@ -1,6 +1,10 @@
<template id="lnbits-qrcode-scanner"> <template id="lnbits-qrcode-scanner">
<q-dialog v-model="showScanner" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl"> :show="showScanner"
:cancel-label="$t('cancel')"
@update:show="showScanner = $event"
@cancel="reset"
>
<div class="text-center q-mb-lg"> <div class="text-center q-mb-lg">
<qrcode-stream <qrcode-stream
@detect="detect" @detect="detect"
@@ -8,15 +12,5 @@
class="rounded-borders" class="rounded-borders"
></qrcode-stream> ></qrcode-stream>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
@click="reset"
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
@@ -8,9 +8,17 @@
icon="edit" icon="edit"
style="position: relative; left: -15px; bottom: -10px" style="position: relative; left: -15px; bottom: -10px"
></q-btn> ></q-btn>
<q-dialog v-model="icon.show" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="icon.show"
<q-form @submit="setIcon" class="q-gutter-md"> :action="{
label: 'Save Icon',
disable: !icon.data.icon
}"
@update:show="icon.show = $event"
@action="setIcon"
@hide="icon.data = {}"
>
<q-form class="q-gutter-md">
<div class="q-gutter-sm q-pa-sm flex flex-wrap justify-center"> <div class="q-gutter-sm q-pa-sm flex flex-wrap justify-center">
<q-btn <q-btn
v-for="(thisIcon, index) in icon.options" v-for="(thisIcon, index) in icon.options"
@@ -41,16 +49,7 @@
class="q-mr-xs" class="q-mr-xs"
></q-btn> ></q-btn>
</div> </div>
<q-btn
unelevated
color="primary"
:disable="!icon.data.icon"
type="submit"
>
Save Icon
</q-btn>
</div> </div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
</template> </template>
@@ -1,11 +1,24 @@
<template id="lnbits-wallet-new"> <template id="lnbits-wallet-new">
<q-dialog v-model="showNewWalletDialog" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-md lnbits__dialog-card"> :show="showNewWalletDialog"
<q-card-section> :title="$t('add_new_wallet')"
<div class="text-h6"> :action="{
<span v-text="$t('add_new_wallet')"></span> label: $t('add_wallet'),
</div> closePopup: true
</q-card-section> }"
:secondary-action="
isLightningShared
? {
label: $t('reject_wallet'),
color: 'negative',
closePopup: true
}
: null
"
@update:show="showNewWalletDialog = $event"
@action="submitAddWallet"
@secondary-action="submitRejectWalletInvitation"
>
<q-card-section v-if="g.user.walletInvitesCount"> <q-card-section v-if="g.user.walletInvitesCount">
<q-badge <q-badge
@click="g.newWalletType = 'lightning-shared'" @click="g.newWalletType = 'lightning-shared'"
@@ -55,40 +68,5 @@
<span v-text="$t('shared_wallet_desc')" class="q-mt-lg"></span> <span v-text="$t('shared_wallet_desc')" class="q-mt-lg"></span>
</div> </div>
</q-card-section> </q-card-section>
</lnbits-dialog>
<q-card-actions class="text-primary">
<div class="row full-width">
<div class="col-md-4">
<q-btn
flat
:label="$t('cancel')"
class="float-left"
v-close-popup
></q-btn>
</div>
<div class="col-md-4">
<q-btn
v-if="isLightningShared"
:disabled="!wallet.sharedWalletId"
flat
:label="$t('reject_wallet')"
v-close-popup
color="negative"
@click="submitRejectWalletInvitation()"
></q-btn>
<span v-else></span>
</div>
<div class="col-md-4">
<q-btn
flat
:label="$t('add_wallet')"
v-close-popup
@click="submitAddWallet()"
class="float-right"
></q-btn>
</div>
</div>
</q-card-actions>
</q-card>
</q-dialog>
</template> </template>
+60 -99
View File
@@ -1231,10 +1231,16 @@
</div> </div>
</div> </div>
<q-dialog v-model="apiAcl.showPasswordDialog" position="top"> <lnbits-dialog
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :show="apiAcl.showPasswordDialog"
<strong>User Password</strong> title="Password Required"
<div class="row q-mt-md q-col-gutter-md"> :action="{
label: $t('ok')
}"
@update:show="apiAcl.showPasswordDialog = $event"
@action="runPasswordGuardedFunction()"
>
<div class="row q-col-gutter-md">
<div class="col-12"> <div class="col-12">
<q-input <q-input
v-model="apiAcl.password" v-model="apiAcl.password"
@@ -1247,55 +1253,39 @@
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
<q-btn
@click="runPasswordGuardedFunction()"
:label="$t('ok')"
color="primary"
></q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="apiAcl.showNewAclDialog" position="top"> <lnbits-dialog
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :show="apiAcl.showNewAclDialog"
<strong>New API Access Control List</strong> title="New API Access Control List"
<div class="row q-mt-md q-col-gutter-md"> :action="{
label: 'Create'
}"
:cancel-label="$t('close')"
@update:show="apiAcl.showNewAclDialog = $event"
@action="addApiACL()"
>
<div class="row q-col-gutter-md">
<div class="col-12"> <div class="col-12">
<q-input v-model="apiAcl.newAclName" dense filled label="ACL Name"> <q-input v-model="apiAcl.newAclName" dense filled label="ACL Name">
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn @click="addApiACL()" label="Create" color="primary"></q-btn>
<q-btn <lnbits-dialog
v-close-popup :show="apiAcl.showNewTokenDialog"
flat title="New API Token"
color="grey" :action="{
class="q-ml-auto" label: 'Create'
v-text="$t('close')" }"
></q-btn> :cancel-label="$t('close')"
</div> @update:show="apiAcl.showNewTokenDialog = $event"
</q-card> @action="generateApiToken()"
</q-dialog>
<q-dialog v-model="apiAcl.showNewTokenDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>New API Token</strong>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12">
<q-input
v-model="apiAcl.newTokenName"
dense
filled
label="Token Name"
> >
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input v-model="apiAcl.newTokenName" dense filled label="Token Name">
</q-input> </q-input>
</div> </div>
<div class="col-12"> <div class="col-12">
@@ -1313,10 +1303,7 @@
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
> >
<q-date <q-date v-model="apiAcl.newTokenExpiry" mask="YYYY-MM-DD HH:mm">
v-model="apiAcl.newTokenExpiry"
mask="YYYY-MM-DD HH:mm"
>
<div class="row items-center justify-end"> <div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat /> <q-btn v-close-popup label="Close" color="primary" flat />
</div> </div>
@@ -1347,26 +1334,28 @@
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
@click="generateApiToken()" <lnbits-dialog
label="Create" :show="labelsDialog.show"
color="primary" :title="$t('label')"
></q-btn> :action="{
<q-btn label: g.user.extra.labels.some(
v-close-popup label => label.name === labelsDialog.data.name
flat )
color="grey" ? $t('update_label')
class="q-ml-auto" : $t('add_label'),
v-text="$t('close')" disable: !labelsDialog.data.name || !labelsDialog.data.color
></q-btn> }"
</div> :cancel-label="$t('cancel')"
</q-card> @update:show="labelsDialog.show = $event"
</q-dialog> @action="
<q-dialog v-model="labelsDialog.show" position="top"> g.user.extra.labels.some(label => label.name === labelsDialog.data.name)
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> ? updateUserLabel()
<strong v-text="$t('label')"></strong> : addUserLabel()
<div class="row q-mt-md q-col-gutter-md"> "
>
<div class="row q-col-gutter-md">
<div class="col-12"> <div class="col-12">
<q-input <q-input
v-model="labelsDialog.data.name" v-model="labelsDialog.data.name"
@@ -1411,33 +1400,5 @@
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
<q-btn
v-if="
g.user.extra.labels.some(
label => label.name === labelsDialog.data.name
)
"
@click="updateUserLabel()"
:disable="!labelsDialog.data.name || !labelsDialog.data.color"
:label="$t('update_label')"
color="primary"
></q-btn>
<q-btn
v-else
@click="addUserLabel()"
:disable="!labelsDialog.data.name || !labelsDialog.data.color"
:label="$t('add_label')"
color="primary"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
+12 -22
View File
@@ -170,9 +170,17 @@
</div> </div>
</div> </div>
<q-dialog v-model="auditDetailsDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :show="auditDetailsDialog.show"
<strong v-text="$t('http_request_details')"></strong> :title="$t('http_request_details')"
:action="{
label: $t('copy'),
icon: 'content_copy'
}"
:cancel-label="$t('close')"
@update:show="auditDetailsDialog.show = $event"
@action="utils.copyText(auditDetailsDialog.data)"
>
<q-input <q-input
filled filled
dense dense
@@ -180,23 +188,5 @@
type="textarea" type="textarea"
rows="25" rows="25"
></q-input> ></q-input>
</lnbits-dialog>
<div class="row q-mt-lg">
<q-btn
@click="utils.copyText(auditDetailsDialog.data)"
icon="content_copy"
color="grey"
flat
v-text="$t('copy')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
+44 -59
View File
@@ -366,9 +366,16 @@
</div> </div>
</div> </div>
<q-dialog v-model="showUninstallDialog" position="top"> <lnbits-dialog
<q-card class="q-pa-lg"> :show="showUninstallDialog"
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6> :title="$t('warning')"
:action="{
label: $t('uninstall_confirm')
}"
:cancel-label="$t('cancel')"
@update:show="showUninstallDialog = $event"
@action="uninstallExtension()"
>
<p> <p>
<span v-text="$t('extension_uninstall_warning')"></span><br /> <span v-text="$t('extension_uninstall_warning')"></span><br />
<span v-text="$t('confirm_continue')"></span> <span v-text="$t('confirm_continue')"></span>
@@ -385,27 +392,21 @@
</q-tooltip> </q-tooltip>
</q-checkbox> </q-checkbox>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
outline
color="grey"
@click="uninstallExtension()"
v-text="$t('uninstall_confirm')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="showDropDbDialog" position="top"> <lnbits-dialog
<q-card v-if="selectedExtension" class="q-pa-lg"> v-if="selectedExtension"
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6> :show="showDropDbDialog"
:title="$t('warning')"
:action="{
label: $t('confirm'),
color: 'red',
disable: dropDbExtensionId !== selectedExtension.id
}"
:cancel-label="$t('cancel')"
@update:show="showDropDbDialog = $event"
@action="dropExtensionDb()"
>
<p><span v-text="$t('extension_db_drop_warning')"></span><br /></p> <p><span v-text="$t('extension_db_drop_warning')"></span><br /></p>
<q-input <q-input
v-model="dropDbExtensionId" v-model="dropDbExtensionId"
@@ -430,8 +431,7 @@
v-text="$t('cancel')" v-text="$t('cancel')"
></q-btn> ></q-btn>
</div> </div>
</q-card> </lnbits-dialog>
</q-dialog>
<q-dialog v-model="showManageExtensionDialog" position="top"> <q-dialog v-model="showManageExtensionDialog" position="top">
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card"> <q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
@@ -1244,9 +1244,11 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="paymentDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-md lnbits__dialog-card"> :show="paymentDialog.show"
<q-card-section> :cancel-label="$t('close')"
@update:show="paymentDialog.show = $event"
>
<q-responsive :ratio="1" class="q-mx-xl q-mb-xl"> <q-responsive :ratio="1" class="q-mx-xl q-mb-xl">
<lnbits-qrcode <lnbits-qrcode
:value="paymentDialog.invoice" :value="paymentDialog.invoice"
@@ -1254,21 +1256,20 @@
class="rounded-borders" class="rounded-borders"
></lnbits-qrcode> ></lnbits-qrcode>
</q-responsive> </q-responsive>
</q-card-section> </lnbits-dialog>
<q-card-actions align="between">
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn> <lnbits-dialog
</q-card-actions> :show="showUpdateAllDialog"
</q-card> :title="$t('update')"
</q-dialog> :action="{
<q-dialog v-model="showUpdateAllDialog" position="top"> label: $t('update')
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> }"
<div class="row"> :cancel-label="$t('cancel')"
<div class="col-12"> @update:show="showUpdateAllDialog = $event"
<h6 class="q-my-md" v-text="$t('update')"></h6> @action="updateSelectedExtensions()"
</div> >
</div>
<div v-if="updatableExtensions?.length > 1" class="row"> <div v-if="updatableExtensions?.length > 1" class="row">
<div class="col-12"> <div class="col-12 q-mb-md">
<q-btn <q-btn
outline outline
color="grey" color="grey"
@@ -1299,21 +1300,5 @@
</div> </div>
</template> </template>
</q-virtual-scroll> </q-virtual-scroll>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
@click="updateSelectedExtensions()"
outline
color="grey"
v-text="$t('update')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
+43 -80
View File
@@ -103,8 +103,15 @@
</q-card-section> </q-card-section>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="channels"> <q-tab-panel name="channels">
<q-dialog v-model="connectPeerDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="connectPeerDialog.show"
:action="{
label: $t('connect')
}"
:cancel-label="$t('cancel')"
@update:show="connectPeerDialog.show = $event"
@action="connectPeer"
>
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
<q-input <q-input
dense dense
@@ -114,28 +121,19 @@
label="Node URI" label="Node URI"
hint="pubkey@host:port" hint="pubkey@host:port"
></q-input> ></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('connect')"
color="primary"
@click="connectPeer"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
<q-dialog v-model="setFeeDialog.show"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="setFeeDialog.show"
<label class="text-h6">Set Channel Fee</label> title="Set Channel Fee"
:action="{
label: $t('set')
}"
:cancel-label="$t('cancel')"
@update:show="setFeeDialog.show = $event"
@action="setChannelFee(setFeeDialog.channel_id)"
>
<p class="text-caption" v-text="setFeeDialog.channel_id"></p> <p class="text-caption" v-text="setFeeDialog.channel_id"></p>
<q-separator></q-separator> <q-separator></q-separator>
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
@@ -153,27 +151,18 @@
v-model.number="setFeeDialog.data.fee_base_msat" v-model.number="setFeeDialog.data.fee_base_msat"
label="Fee Base msat" label="Fee Base msat"
></q-input> ></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('set')"
color="primary"
@click="setChannelFee(setFeeDialog.channel_id)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
<q-dialog v-model="openChannelDialog.show"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="openChannelDialog.show"
:action="{
label: $t('open')
}"
:cancel-label="$t('cancel')"
@update:show="openChannelDialog.show = $event"
@action="openChannel"
>
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
<q-input <q-input
dense dense
@@ -197,9 +186,7 @@
dense dense
type="number" type="number"
filled filled
v-model.number=" v-model.number="openChannelDialog.data.push_amount"
openChannelDialog.data.push_amount
"
label="Push Amount" label="Push Amount"
hint="This gifts sats to the other side!" hint="This gifts sats to the other side!"
></q-input> ></q-input>
@@ -215,27 +202,19 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<div class="row q-mt-lg">
<q-btn
:label="$t('open')"
color="primary"
@click="openChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
<q-dialog v-model="closeChannelDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="closeChannelDialog.show"
title="Close Channel"
:action="{
label: $t('close')
}"
:cancel-label="$t('cancel')"
@update:show="closeChannelDialog.show = $event"
@action="closeChannel"
>
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
<div> <div>
<q-checkbox <q-checkbox
@@ -243,24 +222,8 @@
label="Force" label="Force"
/> />
</div> </div>
<div class="row q-mt-lg">
<q-btn
:label="$t('close')"
color="primary"
@click="closeChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<div class="row q-col-gutter-lg"> <div class="row q-col-gutter-lg">
+13 -19
View File
@@ -446,9 +446,17 @@
</div> </div>
</div> </div>
<div v-else-if="activeWallet.show"> <div v-else-if="activeWallet.show">
<q-dialog v-model="createWalletDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :show="createWalletDialog.show"
<strong>Create Wallet</strong> title="Create Wallet"
:action="{
label: 'Create',
closePopup: true
}"
cancel-label="Cancel"
@update:show="createWalletDialog.show = $event"
@action="createWallet"
>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="row q-mt-lg"> <div class="row q-mt-lg">
@@ -469,27 +477,13 @@
filled filled
dense dense
v-model="createWalletDialog.data.currency" v-model="createWalletDialog.data.currency"
:options="{{ currencies | safe }}" :options="g.allowedCurrencies"
></q-select> ></q-select>
</div> </div>
</div> </div>
<div class="row q-mt-lg">
<q-btn
v-close-popup
@click="createWallet()"
unelevated
color="primary"
type="submit"
>Create</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div> </div>
</div> </div>
</div> </lnbits-dialog>
</q-card>
</q-dialog>
</div> </div>
<div v-else> <div v-else>
<div class="row q-col-gutter-md q-mb-md"> <div class="row q-col-gutter-md q-mb-md">
-4
View File
@@ -15,11 +15,7 @@ from lnbits.settings import settings
def log_server_info(): def log_server_info():
logger.info("LNbits Info") logger.info("LNbits Info")
if settings.first_install: if settings.first_install:
if settings.has_first_install_token_changed():
logger.success("This is a first install token reset.")
else:
logger.success("This is a fresh install of LNbits.") logger.success("This is a fresh install of LNbits.")
if settings.first_install_token: if settings.first_install_token:
logger.success( logger.success(
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. " f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
+1
View File
@@ -123,6 +123,7 @@
"js/components/admin/lnbits-admin-site-customisation.js", "js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-assets-config.js", "js/components/admin/lnbits-admin-assets-config.js",
"js/components/admin/lnbits-admin-audit.js", "js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-dialog.js",
"js/components/lnbits-wallet-charts.js", "js/components/lnbits-wallet-charts.js",
"js/components/lnbits-wallet-api-docs.js", "js/components/lnbits-wallet-api-docs.js",
"js/components/lnbits-wallet-icon.js", "js/components/lnbits-wallet-icon.js",
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "lnbits" name = "lnbits"
version = "1.5.3" version = "1.5.2-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" }]
-1
View File
@@ -71,7 +71,6 @@ async def app(settings: Settings):
username="superadmin", username="superadmin",
password="secret1234", password="secret1234",
password_repeat="secret1234", password_repeat="secret1234",
first_install_token=settings.first_install_token,
) )
) )
-140
View File
@@ -1,140 +0,0 @@
from pathlib import Path
from uuid import uuid4
import pytest
from lnbits.core.crud import create_account, delete_account, get_account
from lnbits.core.crud.settings import get_settings_field, set_settings_field
from lnbits.core.db import db
from lnbits.core.models import Account, UpdateSuperuserPassword, UserExtra
from lnbits.core.services.users import check_admin_settings
from lnbits.core.views.auth_api import first_install
from lnbits.settings import settings
async def _restore_setting_field(field_name: str, original_row) -> None:
if original_row is None:
await db.execute(
"DELETE FROM system_settings WHERE id = :id AND tag = :tag",
{"id": field_name, "tag": "core"},
)
return
await set_settings_field(field_name, original_row.value, original_row.tag)
def test_has_first_install_token_changed_requires_a_confirmed_mismatch():
original_token = settings.first_install_token
original_confirmed = settings.first_install_token_confirmed
try:
settings.first_install_token = "new-token"
settings.first_install_token_confirmed = "old-token"
assert settings.has_first_install_token_changed() is True
settings.first_install_token_confirmed = "new-token"
assert settings.has_first_install_token_changed() is False
settings.first_install_token_confirmed = None
assert settings.has_first_install_token_changed() is False
settings.first_install_token = None
assert settings.has_first_install_token_changed() is False
finally:
settings.first_install_token = original_token
settings.first_install_token_confirmed = original_confirmed
@pytest.mark.anyio
async def test_first_install_confirms_first_install_token(app):
temp_super_user = uuid4().hex
username = f"super_{temp_super_user[:8]}"
original_super_user = settings.super_user
original_first_install = settings.first_install
original_first_install_token = settings.first_install_token
original_first_install_token_confirmed = settings.first_install_token_confirmed
original_confirmed_row = await get_settings_field("first_install_token_confirmed")
await create_account(Account(id=temp_super_user, extra=UserExtra(provider="env")))
try:
settings.super_user = temp_super_user
settings.first_install = True
settings.first_install_token = "expected-token"
settings.first_install_token_confirmed = None
response = await first_install(
UpdateSuperuserPassword(
username=username,
password="secret1234",
password_repeat="secret1234",
first_install_token="expected-token",
)
)
assert response.status_code == 200
assert settings.first_install is False
assert settings.first_install_token_confirmed == "expected-token"
confirmed_row = await get_settings_field("first_install_token_confirmed")
assert confirmed_row is not None
assert confirmed_row.value == "expected-token"
account = await get_account(temp_super_user)
assert account is not None
assert account.username == username
assert account.extra.provider == "lnbits"
assert account.verify_password("secret1234")
finally:
await _restore_setting_field(
"first_install_token_confirmed", original_confirmed_row
)
settings.super_user = original_super_user
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
settings.first_install_token_confirmed = original_first_install_token_confirmed
await delete_account(temp_super_user)
@pytest.mark.anyio
async def test_check_admin_settings_clears_persisted_super_user_when_token_changes(app):
temp_super_user = uuid4().hex
original_super_user = settings.super_user
original_first_install = settings.first_install
original_first_install_token = settings.first_install_token
original_first_install_token_confirmed = settings.first_install_token_confirmed
original_super_user_row = await get_settings_field("super_user")
original_confirmed_row = await get_settings_field("first_install_token_confirmed")
super_user_file = Path(settings.lnbits_data_folder) / ".super_user"
await create_account(
Account(id=temp_super_user, extra=UserExtra(provider="lnbits"))
)
try:
await set_settings_field("super_user", temp_super_user)
await set_settings_field("first_install_token_confirmed", "old-token")
settings.lnbits_admin_ui = True
settings.super_user = temp_super_user
settings.first_install = False
settings.first_install_token = "new-token"
settings.first_install_token_confirmed = "old-token"
await check_admin_settings()
super_user_row = await get_settings_field("super_user")
assert super_user_row is not None
assert super_user_row.value
assert settings.first_install is True
finally:
await _restore_setting_field("super_user", original_super_user_row)
await _restore_setting_field(
"first_install_token_confirmed", original_confirmed_row
)
settings.super_user = original_super_user
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
settings.first_install_token_confirmed = original_first_install_token_confirmed
super_user_file.write_text(original_super_user)
await delete_account(temp_super_user)
Generated
+1 -1
View File
@@ -1247,7 +1247,7 @@ wheels = [
[[package]] [[package]]
name = "lnbits" name = "lnbits"
version = "1.5.3" version = "1.5.2rc1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },