Compare commits

...
Author SHA1 Message Date
Arc 3372f7463a defaults 2025-12-17 00:45:33 +00:00
Arc 3f4fd93858 boltz funding source seed fix
Users should be able to have a seed created or replace with their own
2025-12-17 00:42:39 +00:00
arcbtc 8325d880cb Extra precaution
Just in case someone in lnbits org creates a test release on a fork
2025-12-16 13:45:52 +00:00
dni ⚡andGitHub b090470fc1 fix: CI getting release upload url (#3667) 2025-12-16 14:12:00 +01:00
dni ⚡andGitHub c7297d2e77 feat: add the ability to print qrcodes to <lnbits-qrcode> component (#3664) 2025-12-16 12:12:11 +00:00
dni ⚡andGitHub b6e111b21c fix: dont log routes.json 404 (#3665) 2025-12-16 13:10:02 +01:00
dni ⚡andGitHub 7747d7b741 fix: path check for ext pages with arguments (#3660) 2025-12-11 13:56:49 +01:00
Tiago VasconcelosandGitHub f3a5a8e002 fix: account dropdown (#3658) 2025-12-11 13:17:17 +01:00
Tiago VasconcelosandGitHub 157a6485b4 fix: restore the blur on drawer (#3654) 2025-12-11 13:16:32 +01:00
dni ⚡andGitHub 8867b27b09 fix: LOCALE global variable still used in extension (#3659) 2025-12-11 13:03:38 +01:00
dni ⚡andGitHub 68b607ecbc fix: add spacing to the logo (#3653) 2025-12-10 08:08:00 +01:00
13 changed files with 110 additions and 22 deletions
+5 -1
View File
@@ -22,10 +22,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }} tag: ${{ github.ref_name }}
run: | run: |
upload_url=$(gh release create "$tag" --generate-notes --draft --json upload_url -q '.upload_url') gh release create "$tag" --generate-notes --draft
upload_url=$(gh release view "$tag" --repo "${{ github.repository }}" --json uploadUrl -q ".uploadUrl")
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT" echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
docker: docker:
if: github.repository == 'lnbits/lnbits'
needs: [ release ] needs: [ release ]
uses: ./.github/workflows/docker.yml uses: ./.github/workflows/docker.yml
with: with:
@@ -35,6 +37,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
docker-latest: docker-latest:
if: github.repository == 'lnbits/lnbits'
needs: [ release ] needs: [ release ]
uses: ./.github/workflows/docker.yml uses: ./.github/workflows/docker.yml
with: with:
@@ -44,6 +47,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
pypi: pypi:
if: github.repository == 'lnbits/lnbits'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Install dependencies for building secp256k1 - name: Install dependencies for building secp256k1
+3 -1
View File
@@ -138,7 +138,9 @@ def register_exception_handlers(app: FastAPI): # noqa: C901
@app.exception_handler(404) @app.exception_handler(404)
async def error_handler_404(request: Request, exc: HTTPException): async def error_handler_404(request: Request, exc: HTTPException):
logger.error(f"404: {request.url.path} {exc.status_code}: {exc.detail}")
if not request.url.path.endswith("routes.json"):
logger.error(f"404: {request.url.path} {exc.status_code}: {exc.detail}")
if not _is_browser_request(request): if not _is_browser_request(request):
return JSONResponse( return JSONResponse(
+19 -1
View File
@@ -6,6 +6,7 @@ import inspect
import json import json
import os import os
import re import re
import secrets
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum from enum import Enum
from os import path from os import path
@@ -18,6 +19,21 @@ from loguru import logger
from pydantic import BaseModel, BaseSettings, Extra, Field, validator from pydantic import BaseModel, BaseSettings, Extra, Field, validator
def generate_default_boltz_mnemonic() -> str | None:
"""
Generate a BIP39 mnemonic using the bundled embit dependency.
If embit is unavailable at runtime, return None to avoid blocking startup.
"""
try:
from embit import bip39
except Exception as exc: # pragma: no cover - dependency missing at runtime
logger.warning(f"Unable to generate Boltz mnemonic (embit missing): {exc}")
return None
entropy = secrets.token_bytes(16) # 128 bits -> 12-word mnemonic
return bip39.mnemonic_from_bytes(entropy)
def list_parse_fallback(v: str): def list_parse_fallback(v: str):
v = v.replace(" ", "") v = v.replace(" ", "")
if len(v) > 0: if len(v) > 0:
@@ -604,7 +620,9 @@ class BoltzFundingSource(LNbitsSettings):
boltz_client_wallet: str | None = Field(default="lnbits") boltz_client_wallet: str | None = Field(default="lnbits")
boltz_client_password: str = Field(default="") boltz_client_password: str = Field(default="")
boltz_client_cert: str | None = Field(default=None) boltz_client_cert: str | None = Field(default=None)
boltz_mnemonic: str | None = Field(default=None) boltz_mnemonic: str | None = Field(
default_factory=generate_default_boltz_mnemonic
)
class StrikeFundingSource(LNbitsSettings): class StrikeFundingSource(LNbitsSettings):
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -217,6 +217,7 @@ body.body--dark .q-header,
body.body--dark .q-drawer { body.body--dark .q-drawer {
--q-dark: rgba(29, 29, 29, 0.3); --q-dark: rgba(29, 29, 29, 0.3);
background-color: var(--q-dark); background-color: var(--q-dark);
backdrop-filter: blur(6px) brightness(0.8);
} }
:root { :root {
@@ -161,15 +161,21 @@ window.app.component('lnbits-admin-funding-sources', {
'Boltz', 'Boltz',
{ {
boltz_client_endpoint: 'Endpoint', boltz_client_endpoint: 'Endpoint',
boltz_client_macaroon: 'Admin Macaroon path or hex', boltz_client_macaroon: {
boltz_client_cert: 'Certificate path or hex', label: 'Admin Macaroon path or hex',
default: '/home/ubuntu/.boltz/macaroons/admin.macaroon'
},
boltz_client_cert: {
label: 'Certificate path or hex',
default: '/home/ben/.boltz/tls.cert'
},
boltz_client_wallet: 'Wallet Name', boltz_client_wallet: 'Wallet Name',
boltz_client_password: 'Wallet Password (can be empty)', boltz_client_password: 'Wallet Password (can be empty)',
boltz_mnemonic: { boltz_mnemonic: {
label: 'Liquid mnemonic (copy into greenwallet)', label: 'Liquid mnemonic (copy into greenwallet)',
readonly: true,
copy: true, copy: true,
qrcode: true qrcode: true,
hint: 'Paste or type your liquid mnemonic'
} }
} }
], ],
@@ -13,6 +13,10 @@ window.app.component('lnbits-qrcode', {
type: Boolean, type: Boolean,
default: false default: false
}, },
print: {
type: Boolean,
default: false
},
showButtons: { showButtons: {
type: Boolean, type: Boolean,
default: true default: true
@@ -41,6 +45,41 @@ window.app.component('lnbits-qrcode', {
} }
}, },
methods: { methods: {
printQrCode() {
const svg = this.$refs.qrCode.$el.outerHTML
const printWindow = window.open('', '_blank')
printWindow.document.write(`
<html>
<head>
<title>Print QR Code</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
img {
position: absolute;
max-width: 51px;
width: 14%;
overflow: hidden;
background: #fff;
overflow: hidden;
padding: -1.2rem;
border-radius: -1.2rem;
}
</style>
</head>
<body>${svg}<img src="${this.logo}"></body>
</html>
`)
printWindow.document.close()
printWindow.focus()
printWindow.print()
printWindow.close()
},
clickQrCode(event) { clickQrCode(event) {
if (this.href === '') { if (this.href === '') {
this.utils.copyText(this.value) this.utils.copyText(this.value)
+4 -1
View File
@@ -38,7 +38,7 @@ const DynamicComponent = {
let route = RENDERED_ROUTE let route = RENDERED_ROUTE
// append trailing slash only on the root path `/path` -> `/path/` // append trailing slash only on the root path `/path` -> `/path/`
if (route.split('/').length === 2) route += '/' if (route.split('/').length === 2) route += '/'
if (route !== this.$route.fullPath) { if (route !== this.$route.path) {
console.log('Redirecting to non-vue route:', this.$route.fullPath) console.log('Redirecting to non-vue route:', this.$route.fullPath)
window.location = this.$route.fullPath window.location = this.$route.fullPath
return return
@@ -138,6 +138,9 @@ window.router = VueRouter.createRouter({
routes routes
}) })
// BACKWARDS compatibility extensions
window.LOCALE = window.g.locale
window.i18n = new VueI18n.createI18n({ window.i18n = new VueI18n.createI18n({
locale: window.g.locale, locale: window.g.locale,
fallbackLocale: 'en', fallbackLocale: 'en',
+1
View File
@@ -63,5 +63,6 @@ body.body--dark {
.q-drawer { .q-drawer {
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)}; --q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
background-color: var(--q-dark); background-color: var(--q-dark);
backdrop-filter: blur(6px) brightness(0.8);
} }
} }
+1 -1
View File
@@ -534,7 +534,7 @@ include('components/lnbits-error.vue') %}
<q-tab name="bech32" icon="qr_code" label="bech32"></q-tab> <q-tab name="bech32" icon="qr_code" label="bech32"></q-tab>
<q-tab name="lud17" icon="link" label="url (lud17)"></q-tab> <q-tab name="lud17" icon="link" label="url (lud17)"></q-tab>
</q-tabs> </q-tabs>
<lnbits-qrcode :value="lnurl" nfc="true"></lnbits-qrcode> <lnbits-qrcode :value="lnurl" nfc="true" print="true"></lnbits-qrcode>
</div> </div>
</template> </template>
+15 -11
View File
@@ -22,7 +22,7 @@
@click="g.visibleDrawer = !g.visibleDrawer" @click="g.visibleDrawer = !g.visibleDrawer"
></q-btn> ></q-btn>
<q-toolbar-title> <q-toolbar-title>
<q-btn flat no-caps dense size="lg" type="a" href="/"> <q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
<q-img <q-img
v-if="customLogoUrl" v-if="customLogoUrl"
height="30px" height="30px"
@@ -73,32 +73,36 @@
<lnbits-language-dropdown></lnbits-language-dropdown> <lnbits-language-dropdown></lnbits-language-dropdown>
<q-btn-dropdown <q-btn-dropdown v-if="g.user" flat rounded size="sm" class="q-pl-sm">
v-if="g.user || g.isUserAuthorized"
flat
rounded
size="sm"
class="q-pl-sm"
>
<template v-slot:label> <template v-slot:label>
<q-avatar v-if="g.user?.extra?.picture !== ''" size="18px"> <q-avatar
v-if="g.user?.extra?.picture && g.user?.extra?.picture !== ''"
size="18px"
>
<q-img :src="g.user?.extra?.picture"></q-img> <q-img :src="g.user?.extra?.picture"></q-img>
</q-avatar> </q-avatar>
<q-avatar v-else icon="account_circle" size="18px"></q-avatar> <q-avatar v-else icon="account_circle" size="18px"></q-avatar>
</template> </template>
<q-list style="max-width: 200px"> <q-list style="max-width: 200px">
<q-item> <q-item>
<q-item-section avatar v-if="g.user?.extra?.picture !== ''"> <q-item-section
avatar
v-if="
g.user &&
g.user?.extra?.picture &&
g.user?.extra?.picture !== ''
"
>
<q-avatar size="md"> <q-avatar size="md">
<img :src="g.user?.extra?.picture" /> <img :src="g.user?.extra?.picture" />
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label <q-item-label
caption
class="ellipsis" class="ellipsis"
v-text="displayName" v-text="displayName"
></q-item-label> ></q-item-label>
<q-item-label caption v-text="displayRole"></q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator></q-separator> <q-separator></q-separator>
@@ -44,6 +44,16 @@
> >
<q-tooltip>Write NFC Tag</q-tooltip> <q-tooltip>Write NFC Tag</q-tooltip>
</q-btn> </q-btn>
<q-btn
v-if="print"
flat
dense
class="text-grey"
@click="printQrCode()"
icon="print"
>
<q-tooltip>Print</q-tooltip>
</q-btn>
<q-btn flat dense class="text-grey" icon="download" @click="downloadSVG"> <q-btn flat dense class="text-grey" icon="download" @click="downloadSVG">
<q-tooltip>Download SVG</q-tooltip> <q-tooltip>Download SVG</q-tooltip>
</q-btn> </q-btn>