Compare commits

..
22 Commits
Author SHA1 Message Date
Tiago Vasconcelos f53fee6896 add onboard to wallet 2024-02-15 12:18:52 +00:00
Tiago Vasconcelos f14dfd2522 user onboarding 2024-02-15 09:23:09 +00:00
526467747e Add Keycloak SSO (#2272)
* feat: add `keycloak` SSO

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-02-14 09:23:37 +01:00
Vlad StanandGitHub b8d295a5b7 refactor: generalize SSO auth (#2263)
* refactor: first extraction of providers
* refactor: remove unused property
* refactor: extract `_find_auth_provider_class`
* fix: return type
* feat: prepare for `keycloak`
2024-02-14 07:57:50 +01:00
jackstar12andGitHub 293b6267be fix: correct invoice expiry for lndrest (#2271) 2024-02-13 15:14:09 +02:00
Vlad StanandGitHub 20e70854a1 fix: add installed extensions to the ext list if missing (#2261) 2024-02-12 15:33:28 +01:00
dni ⚡andGitHub 11173e6460 fix: github release missing checkout (#2264)
actions/checkout was missing so it could not release
2024-02-12 13:00:03 +01:00
dni ⚡andGitHub 069a7922fc chore: update to 0.12.1 (#2262) 2024-02-12 12:43:20 +01:00
98338ffb00 Improves payment reactions (#2251)
* updated to select

* updating

* working

* bundle

* updated to select

* updating

* working

* bundle

* Make room for everything apart from sending

* Update lnbits/static/js/base.js

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>

* prettier

* bundled

* fundle

* changed to window[]

* vlads suggestion

* added stars

* vlads suggestion

* fundle

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-12 10:48:07 +00:00
Pavol Rusnak 62dec118af chore: make bundle 2024-02-09 12:36:07 -03:00
tlindiandPavol Rusnak 84b406534d Update fi.js 2024-02-09 12:36:07 -03:00
d9d2d59b73 Wallet limits: max balance, daily max withdraw, transactions per sec (#2223)
Co-authored-by: benarc <ben@arc.wales>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-09 17:25:53 +02:00
20d4b954c0 fix: custom site desc (#2254)
* custom site desc

* fiddled formatting

---------

Co-authored-by: arcbtc <ben@arc.wales>
2024-02-09 15:08:02 +00:00
dni ⚡andGitHub 5edb845d8f feat: add setting lnbits_allow_new_accounts to admin ui (#2252)
added to users tab of admin ui
2024-02-09 14:54:30 +02:00
cfe2b43d2a Improved env layout (#2226)
* Improved env layout

* Update .env.example

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-09 14:34:10 +02:00
Vlad StanandGitHub a0ac41a9ce fix: use get_dbversions (#2255) 2024-02-09 14:10:51 +02:00
dni ⚡andGitHub 194a527736 fix: regtest use new docker hub address (#2248)
* fix: regtest use new docker hub address
2024-02-09 08:46:33 +01:00
dni ⚡andGitHub 6f135c0696 feat: add monitor admin endpoint for listeners (#2242)
this can be useful when debugging invoice listeners. in the future there coud be some monitor dashboard on the admin ui.
2024-02-09 08:08:37 +01:00
dni ⚡andGitHub 9a767b627f chore: dead code perform_balance_check (#2244)
this code is not used anywhere on core nor on extensions
2024-02-08 15:01:23 +01:00
c1c4eda69d Adds optional confetti to incoming payments in wallet (#2231)
* initial

* Working with toggle

* Prettier and bundle

* removed console.log

* bundle

* fix: i18n

* refactor: simplify logic

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-08 13:34:03 +00:00
dni ⚡andGitHub 6c46867698 chore: update readme to use new docker alias (#2246)
`docker pull lnbits/lnbits`
2024-02-08 13:24:55 +00:00
dni ⚡andGitHub a73eb569ae bug: docker upload workflow (#2245)
something changed in the docker-build-push action.

added `workflow_dispatch` to be able to manually run those docker releases via github
2024-02-08 11:30:38 +01:00
40 changed files with 1575 additions and 960 deletions
+40 -24
View File
@@ -1,5 +1,28 @@
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/ #For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
######################################
########### Admin Settings ###########
######################################
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available.
# Warning: Enabling this will make LNbits ignore most configurations in file. Only the
# configurations defined in `ReadOnlySettings` will still be read from the environment variables.
# The rest of the settings will be stored in your database and you will be able to change them
# only through the Admin UI.
# Disable this to make LNbits use this config file again.
LNBITS_ADMIN_UI=false
# Change theme
LNBITS_SITE_TITLE="LNbits"
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
HOST=127.0.0.1
PORT=5000
###################################### ######################################
########## Funding Source ############ ########## Funding Source ############
###################################### ######################################
@@ -78,48 +101,34 @@ ECLAIR_PASS=eclairpw
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
LNTIPS_API_ENDPOINT=https://ln.tips LNTIPS_API_ENDPOINT=https://ln.tips
######################################
########### Admin Settings ###########
######################################
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
# Warning: Enabling this will make LNbits ignore this configuration file. Your settings will
# be stored in your database and you will be able to change them only through the Admin UI.
# Disable this to make LNbits use this config file again.
LNBITS_ADMIN_UI=false
# Change theme
LNBITS_SITE_TITLE="LNbits"
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
HOST=127.0.0.1
PORT=5000
###################################### ######################################
####### Auth Configurations ########## ####### Auth Configurations ##########
###################################### ######################################
# Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value. # Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value.
AUTH_SECRET_KEY="" AUTH_SECRET_KEY=""
AUTH_TOKEN_EXPIRE_MINUTES=525600 AUTH_TOKEN_EXPIRE_MINUTES=525600
# Possible authorization methods: user-id-only, username-password, google-auth, github-auth # Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth
AUTH_ALLOWED_METHODS="user-id-only, username-password" AUTH_ALLOWED_METHODS="user-id-only, username-password"
# Set this flag if HTTP is used for OAuth # Set this flag if HTTP is used for OAuth
# OAUTHLIB_INSECURE_TRANSPORT="1" # OAUTHLIB_INSECURE_TRANSPORT="1"
# Google OAuth Config # Google OAuth Config
# Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token # Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET="" GOOGLE_CLIENT_SECRET=""
# GitHub OAuth Config # GitHub OAuth Config
# Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token # Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token
GITHUB_CLIENT_ID="" GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET="" GITHUB_CLIENT_SECRET=""
# Keycloak OAuth Config
# Make sure that the valid redirect URIs contain https://{domain}/api/v1/auth/keycloak/token
KEYCLOAK_CLIENT_ID=""
KEYCLOAK_CLIENT_SECRET=""
KEYCLOAK_DISCOVERY_URL=""
###################################### ######################################
# uvicorn variable, uncomment to allow https behind a proxy # uvicorn variable, uncomment to allow https behind a proxy
@@ -198,6 +207,13 @@ LNBITS_RESERVE_FEE_MIN=2000
# value in percent # value in percent
LNBITS_RESERVE_FEE_PERCENT=1.0 LNBITS_RESERVE_FEE_PERCENT=1.0
# limit the maximum balance for each wallet
# throw an error if the wallet attempts to create a new invoice
# LNBITS_WALLET_LIMIT_MAX_BALANCE=1000000
# LNBITS_WALLET_LIMIT_DAILY_MAX_WITHDRAW=1000000
# LNBITS_WALLET_LIMIT_SECS_BETWEEN_TRANS=60
# Limit fiat currencies allowed to see in UI # Limit fiat currencies allowed to see in UI
# LNBITS_ALLOWED_CURRENCIES="EUR, USD" # LNBITS_ALLOWED_CURRENCIES="EUR, USD"
+7 -2
View File
@@ -1,6 +1,11 @@
name: docker name: docker
on: on:
workflow_dispatch:
inputs:
tag:
default: latest
type: string
workflow_call: workflow_call:
inputs: inputs:
tag: tag:
@@ -38,7 +43,7 @@ jobs:
context: . context: .
push: true push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:${{ inputs.tag }} tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:${{ inputs.tag }}
platforms: [ linux/amd64, linux/arm64 ] platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
@@ -48,6 +53,6 @@ jobs:
context: . context: .
push: true push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:latest tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:latest
platforms: [ linux/amd64, linux/arm64 ] platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
+3 -3
View File
@@ -33,8 +33,8 @@ jobs:
with: with:
context: . context: .
push: false push: false
tags: lnbitsdocker/lnbits-legend:latest tags: lnbits/lnbits:latest
cache-from: type=registry,ref=lnbitsdocker/lnbits-legend:latest cache-from: type=registry,ref=lnbits/lnbits:latest
cache-to: type=inline cache-to: type=inline
- name: Setup Regtest - name: Setup Regtest
@@ -51,7 +51,7 @@ jobs:
- name: Create fake admin - name: Create fake admin
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }} if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
run: docker exec lnbits-legend-lnbits-1 poetry run python tools/create_fake_admin.py run: docker exec lnbits-lnbits-1 poetry run python tools/create_fake_admin.py
- name: Run Tests - name: Run Tests
env: env:
+1
View File
@@ -13,6 +13,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3
- name: Create github release - name: Create github release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4 -4
View File
@@ -101,19 +101,19 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
use latest version from docker hub use latest version from docker hub
```sh ```sh
docker pull lnbitsdocker/lnbits-legend docker pull lnbits/lnbits
wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env
mkdir data mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
``` ```
build the image yourself build the image yourself
```sh ```sh
git clone https://github.com/lnbits/lnbits.git git clone https://github.com/lnbits/lnbits.git
cd lnbits cd lnbits
docker build -t lnbitsdocker/lnbits-legend . docker build -t lnbits/lnbits .
cp .env.example .env cp .env.example .env
mkdir data mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
``` ```
## Option 4: Fly.io ## Option 4: Fly.io
+3 -3
View File
@@ -23,7 +23,7 @@ from slowapi.util import get_remote_address
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from lnbits.core.crud import get_installed_extensions from lnbits.core.crud import get_dbversions, get_installed_extensions
from lnbits.core.helpers import migrate_extension_database from lnbits.core.helpers import migrate_extension_database
from lnbits.core.services import websocketUpdater from lnbits.core.services import websocketUpdater
from lnbits.core.tasks import ( # register_watchdog,; unregister_watchdog, from lnbits.core.tasks import ( # register_watchdog,; unregister_watchdog,
@@ -35,7 +35,7 @@ from lnbits.tasks import cancel_all_tasks, create_permanent_task
from lnbits.utils.cache import cache from lnbits.utils.cache import cache
from lnbits.wallets import get_wallet_class, set_wallet_class from lnbits.wallets import get_wallet_class, set_wallet_class
from .commands import db_versions, migrate_databases from .commands import migrate_databases
from .core import init_core_routers from .core import init_core_routers
from .core.db import core_app_extra from .core.db import core_app_extra
from .core.services import check_admin_settings, check_webpush_settings from .core.services import check_admin_settings, check_webpush_settings
@@ -273,7 +273,7 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
extension = Extension.from_installable_ext(ext) extension = Extension.from_installable_ext(ext)
register_ext_routes(app, extension) register_ext_routes(app, extension)
current_version = (await db_versions()).get(ext.id, 0) current_version = (await get_dbversions()).get(ext.id, 0)
await migrate_extension_database(extension, current_version) await migrate_extension_database(extension, current_version)
# mount routes for the new version # mount routes for the new version
+2
View File
@@ -1097,6 +1097,8 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
return None return None
row_dict = dict(sets) row_dict = dict(sets)
row_dict.pop("super_user") row_dict.pop("super_user")
row_dict.pop("auth_all_methods")
admin_settings = AdminSettings( admin_settings = AdminSettings(
is_super_user=is_super_user, is_super_user=is_super_user,
lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources, lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources,
+67 -2
View File
@@ -1,6 +1,7 @@
import asyncio import asyncio
import datetime import datetime
import json import json
import time
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple, TypedDict from typing import Dict, List, Optional, Tuple, TypedDict
@@ -41,6 +42,7 @@ from .crud import (
create_wallet, create_wallet,
delete_wallet_payment, delete_wallet_payment,
get_account, get_account,
get_payments,
get_standalone_payment, get_standalone_payment,
get_super_settings, get_super_settings,
get_total_balance, get_total_balance,
@@ -118,8 +120,9 @@ async def create_invoice(
if not amount > 0: if not amount > 0:
raise InvoiceFailure("Amountless invoices not supported.") raise InvoiceFailure("Amountless invoices not supported.")
if await get_wallet(wallet_id, conn=conn) is None: user_wallet = await get_wallet(wallet_id, conn=conn)
raise InvoiceFailure("Wallet does not exist.") if not user_wallet:
raise InvoiceFailure(f"Could not fetch wallet '{wallet_id}'.")
invoice_memo = None if description_hash else memo invoice_memo = None if description_hash else memo
@@ -130,6 +133,14 @@ async def create_invoice(
amount, wallet_id, currency=currency, extra=extra, conn=conn amount, wallet_id, currency=currency, extra=extra, conn=conn
) )
if settings.is_wallet_max_balance_exceeded(
user_wallet.balance_msat / 1000 + amount_sat
):
raise InvoiceFailure(
f"Wallet balance cannot exceed "
f"{settings.lnbits_wallet_limit_max_balance} sats."
)
ok, checking_id, payment_request, error_message = await wallet.create_invoice( ok, checking_id, payment_request, error_message = await wallet.create_invoice(
amount=amount_sat, amount=amount_sat,
memo=invoice_memo, memo=invoice_memo,
@@ -185,6 +196,8 @@ async def pay_invoice(
if max_sat and invoice.amount_msat > max_sat * 1000: if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.") raise ValueError("Amount in invoice is too high.")
await check_wallet_limits(wallet_id, conn, invoice.amount_msat)
async with db.reuse_conn(conn) if conn else db.connect() as conn: async with db.reuse_conn(conn) if conn else db.connect() as conn:
temp_id = invoice.payment_hash temp_id = invoice.payment_hash
internal_id = f"internal_{invoice.payment_hash}" internal_id = f"internal_{invoice.payment_hash}"
@@ -372,6 +385,58 @@ async def pay_invoice(
return invoice.payment_hash return invoice.payment_hash
async def check_wallet_limits(wallet_id, conn, amount_msat):
await check_time_limit_between_transactions(conn, wallet_id)
await check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat)
async def check_time_limit_between_transactions(conn, wallet_id):
limit = settings.lnbits_wallet_limit_secs_between_trans
if not limit or limit <= 0:
return
payments = await get_payments(
since=int(time.time()) - limit,
wallet_id=wallet_id,
limit=1,
conn=conn,
)
if len(payments) == 0:
return
raise ValueError(
f"The time limit of {limit} seconds between payments has been reached."
)
async def check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat):
limit = settings.lnbits_wallet_limit_daily_max_withdraw
if not limit or limit <= 0:
return
payments = await get_payments(
since=int(time.time()) - 60 * 60 * 24,
outgoing=True,
wallet_id=wallet_id,
limit=1,
conn=conn,
)
if len(payments) == 0:
return
total = 0
for pay in payments:
total += pay.amount
total = total - amount_msat
if limit * 1000 + total < 0:
raise ValueError(
"Daily withdrawal limit of "
+ str(settings.lnbits_wallet_limit_daily_max_withdraw)
+ " sats reached."
)
async def redeem_lnurl_withdraw( async def redeem_lnurl_withdraw(
wallet_id: str, wallet_id: str,
lnurl_request: str, lnurl_request: str,
+37
View File
@@ -0,0 +1,37 @@
"""Keycloak SSO Login Helper
"""
from typing import Optional
import httpx
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
class KeycloakSSO(SSOBase):
"""Class providing login via Keycloak OAuth"""
provider = "keycloak"
scope = ["openid", "email", "profile"]
discovery_url = ""
async def openid_from_response(
self, response: dict, session: Optional["httpx.AsyncClient"] = None
) -> OpenID:
"""Return OpenID from user information provided by Keycloak"""
return OpenID(
email=response.get("email", ""),
provider=self.provider,
id=response.get("sub"),
first_name=response.get("given_name"),
last_name=response.get("family_name"),
display_name=response.get("name"),
picture=response.get("picture"),
)
async def get_discovery_document(self) -> DiscoveryDocument:
"""Get document containing handy urls"""
async with httpx.AsyncClient() as session:
response = await session.get(self.discovery_url)
content = response.json()
return content
@@ -85,7 +85,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="isSuperUser"> <div v-if="isSuperUser" id="funding-sources">
<lnbits-funding-sources <lnbits-funding-sources
:form-data="formData" :form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources" :allowed-funding-sources="settings.lnbits_allowed_funding_sources"
@@ -78,6 +78,41 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('keycloak-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">Keycloak Auth</strong>
<div class="row">
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_discovery_url"
label="Keycloak Discovey URL"
>
</q-input>
</div>
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_client_id"
label="Keycloak Client ID"
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token"
>
</q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_secret"
type="password"
label="Keycloak Client Secret"
>
</q-input>
</div>
</div>
</q-card-section>
<q-separator></q-separator> <q-separator></q-separator>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<br /> <br />
@@ -180,6 +215,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-md-12"> <div class="col-12 col-md-12">
<p v-text="$t('rate_limiter')"></p> <p v-text="$t('rate_limiter')"></p>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
@@ -201,6 +237,36 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-md-12">
<p v-text="$t('wallet_limiter')"></p>
<div class="row q-col-gutter-md">
<div class="col-4">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_max_balance"
:label="$t('wallet_max_ballance')"
></q-input>
</div>
<div class="col-4">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_daily_max_withdraw"
:label="$t('wallet_limit_max_withdraw_per_day')"
></q-input>
</div>
<div class="col-4">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_secs_between_trans"
:label="$t('wallet_limit_secs_between_trans')"
></q-input>
</div>
</div>
</div>
</div> </div>
</div> </div>
<br /> <br />
+20 -6
View File
@@ -16,7 +16,6 @@
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn> <q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input> </q-input>
<div> <div>
{%raw%}
<q-chip <q-chip
v-for="user in formData.lnbits_admin_users" v-for="user in formData.lnbits_admin_users"
:key="user" :key="user"
@@ -24,10 +23,9 @@
@remove="removeAdminUser(user)" @remove="removeAdminUser(user)"
color="primary" color="primary"
text-color="white" text-color="white"
v-text="user"
> >
{{ user }}
</q-chip> </q-chip>
{%endraw%}
</div> </div>
<br /> <br />
</div> </div>
@@ -44,7 +42,6 @@
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn> <q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input> </q-input>
<div> <div>
{% raw %}
<q-chip <q-chip
v-for="user in formData.lnbits_allowed_users" v-for="user in formData.lnbits_allowed_users"
:key="user" :key="user"
@@ -52,12 +49,29 @@
@remove="removeAllowedUser(user)" @remove="removeAllowedUser(user)"
color="primary" color="primary"
text-color="white" text-color="white"
v-text="user"
> >
{{ user }}
</q-chip> </q-chip>
{% endraw %}
</div> </div>
<br /> <br />
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Allow creation of new users</q-item-label>
<q-item-label caption
>Allow creation of new users on the index page</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_allow_new_accounts"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
+6
View File
@@ -165,4 +165,10 @@
</q-dialog> </q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script> <script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
<style>
.introjs-tooltip-custom .introjs-tooltiptext,
.introjs-tooltip-header {
color: #111;
}
</style>
{% endblock %} {% endblock %}
+17
View File
@@ -401,6 +401,23 @@
></lnbits-notifications-btn> ></lnbits-notifications-btn>
</div> </div>
</div> </div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('payment_reactions')"></span>
</div>
<div class="col-8">
<q-select
v-model="reactionChoice"
:options="reactionOptions"
label="Reactions"
@input="reactionChoiceFunc"
>
<q-tooltip
><span v-text="$t('payment_reactions')"></span
></q-tooltip>
</q-select>
</div>
</div>
</q-tab-panel> </q-tab-panel>
</q-tab-panels> </q-tab-panels>
</div> </div>
+49 -26
View File
@@ -4,12 +4,16 @@
<div class="row q-col-gutter-md justify-center"> <div class="row q-col-gutter-md justify-center">
<div <div
v-if="isUserAuthorized" v-if="isUserAuthorized"
class="col-12 col-md-7 col-lg-6 q-gutter-y-md" class="col-12 col-md-6 col-lg-6 q-gutter-y-md"
></div> ></div>
<div v-else class="col-12 col-md-7 col-lg-6 q-gutter-y-md"> <div v-else class="col-12 col-md-4 col-lg-4 q-gutter-y-md">
<div class="gt-sm"> <div class="gt-sm">
<h3 class="q-my-none">{{SITE_TITLE}}</h3> <h3 class="q-my-none" v-if="'{{SITE_TITLE}}' == 'LNbits'">
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5> {{SITE_TITLE}}
</h3>
<h5 class="q-my-md" v-if="'{{SITE_TITLE}}' == 'LNbits'">
{{SITE_TAGLINE}}
</h5>
</div> </div>
{% if lnurl and LNBITS_NEW_ACCOUNTS_ALLOWED and ("user-id-only" in {% if lnurl and LNBITS_NEW_ACCOUNTS_ALLOWED and ("user-id-only" in
LNBITS_AUTH_METHODS)%} LNBITS_AUTH_METHODS)%}
@@ -33,7 +37,7 @@
{%else%} {% endif %} {%else%} {% endif %}
<div class="row q-mt-md"> <div class="row q-mt-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> <div class="col-12 col-md-10 col-lg-10 q-gutter-y-md">
<q-badge v-if="isAccessTokenExpired" color="primary" rounded> <q-badge v-if="isAccessTokenExpired" color="primary" rounded>
<div class="text-h5"> <div class="text-h5">
<span v-text="$t('session_has_expired')"></span> <span v-text="$t('session_has_expired')"></span>
@@ -259,6 +263,25 @@
<div><span v-text="$t('signin_with_github')"></span></div> <div><span v-text="$t('signin_with_github')"></span></div>
</q-btn> </q-btn>
</div> </div>
{%endif%} {% if "keycloak-auth" in LNBITS_AUTH_METHODS %}
<div class="col-12 full-width q-pa-sm">
<q-btn
href="/api/v1/auth/keycloak"
type="a"
outline
no-caps
color="grey"
rounded
class="full-width"
>
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="'{{ static_url_for('static', 'images/keycloak-logo.png') }}'"
></q-img>
</q-avatar>
<div><span v-text="$t('signin_with_keycloak')"></span></div>
</q-btn>
</div>
{%endif%} {%endif%}
</div> </div>
</q-card-section> </q-card-section>
@@ -282,11 +305,16 @@
</div> </div>
</div> </div>
<!-- Ads -->
<div <div
class="col-12 col-md-3 col-lg-3 gt-sm" v-if="'{{SITE_TITLE}}' != 'LNbits'"
v-if="'{{SITE_TITLE}}' == 'LNbits'" class="col-12 col-md-5 col-lg-5 q-pt-xl"
> >
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
<div v-html="formatDescription"></div>
</div>
<div class="col-12 col-md-3 col-lg-3 gt-sm" v-else>
<div class="row q-col-gutter-lg justify-center"> <div class="row q-col-gutter-lg justify-center">
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm"> <div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn <q-btn
@@ -465,27 +493,22 @@
<div class="col q-pl-md"></div> <div class="col q-pl-md"></div>
</div> </div>
</div> </div>
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn flat color="secondary" class="full-width q-mb-md"
>{{ AD_SPACE_TITLE }}</q-btn
>
<a href="{{ AD[0] }}" class="q-ma-md">
<img
v-if="($q.dark.isActive)"
src="{{ AD[1] }}"
style="max-width: 90%"
/>
<img v-else src="{{ AD[2] }}" style="max-width: 90%" />
</a>
</div>
{% endfor %} {% endif %}
</div> </div>
</div> </div>
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn flat color="secondary" class="full-width q-mb-md"
>{{ AD_SPACE_TITLE }}</q-btn
>
<a href="{{ AD[0] }}" class="q-ma-md">
<img v-if="($q.dark.isActive)" src="{{ AD[1] }}" style="max-width: 90%" />
<img v-else src="{{ AD[2] }}" style="max-width: 90%" />
</a>
</div>
{% endfor %} {% endif %}
</div> </div>
<div class="row gt-sm q-mt-xl"> <div v-if="'{{SITE_TITLE}}' == 'LNbits'" class="row gt-sm q-mt-xl">
<div class="col-1"></div> <div class="col-1"></div>
<div class="col-10 q-pl-xl"> <div class="col-10 q-pl-xl">
<span v-text="$t('lnbits_description')"></span> <span v-text="$t('lnbits_description')"></span>
+13 -2
View File
@@ -4,6 +4,12 @@
<!----> <!---->
{% block scripts %} {{ window_vars(user, wallet) }} {% block scripts %} {{ window_vars(user, wallet) }}
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script> <script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
<style>
.introjs-tooltip-custom .introjs-tooltiptext,
.introjs-tooltip-header {
color: #111;
}
</style>
{% endblock %} {% endblock %}
<!----> <!---->
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %} {% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
@@ -34,7 +40,7 @@
} : ''" } : ''"
> >
<q-card-section> <q-card-section>
<h3 class="q-my-none text-no-wrap"> <h3 class="q-my-none text-no-wrap" id="wallet-balance">
<strong v-text="formattedBalance"></strong> <strong v-text="formattedBalance"></strong>
<small>{{LNBITS_DENOMINATION}}</small> <small>{{LNBITS_DENOMINATION}}</small>
<lnbits-update-balance <lnbits-update-balance
@@ -68,7 +74,10 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md gt-sm"> <div
class="row q-pb-md q-px-md q-col-gutter-md gt-sm"
id="wallet-buttons"
>
<div class="col"> <div class="col">
<q-btn <q-btn
unelevated unelevated
@@ -165,6 +174,7 @@
:hide-header="mobileSimple" :hide-header="mobileSimple"
:hide-bottom="mobileSimple" :hide-bottom="mobileSimple"
@request="fetchPayments" @request="fetchPayments"
id="wallet-table"
> >
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
@@ -895,6 +905,7 @@
</q-dialog> </q-dialog>
<q-tabs <q-tabs
id="mobile-wallet-buttons"
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top" class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
active-class="px-0" active-class="px-0"
indicator-color="transparent" indicator-color="transparent"
+15
View File
@@ -17,9 +17,11 @@ from lnbits.core.services import (
update_cached_settings, update_cached_settings,
update_wallet_balance, update_wallet_balance,
) )
from lnbits.core.tasks import api_invoice_listeners
from lnbits.decorators import check_admin, check_super_user from lnbits.decorators import check_admin, check_super_user
from lnbits.server import server_restart from lnbits.server import server_restart
from lnbits.settings import AdminSettings, UpdateSettings, settings from lnbits.settings import AdminSettings, UpdateSettings, settings
from lnbits.tasks import invoice_listeners
from .. import core_app_extra from .. import core_app_extra
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
@@ -48,6 +50,19 @@ async def api_auditor():
) )
@admin_router.get(
"/admin/api/v1/monitor",
name="Monitor",
description="show the current listeners and other monitoring data",
dependencies=[Depends(check_admin)],
)
async def api_monitor():
return {
"invoice_listeners": list(invoice_listeners.keys()),
"api_invoice_listeners": list(api_invoice_listeners.keys()),
}
@admin_router.get("/admin/api/v1/settings/", response_model=Optional[AdminSettings]) @admin_router.get("/admin/api/v1/settings/", response_model=Optional[AdminSettings])
async def api_get_settings( async def api_get_settings(
user: User = Depends(check_admin), user: User = Depends(check_admin),
+66 -86
View File
@@ -1,10 +1,9 @@
from typing import Optional import importlib
from typing import Callable, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse, RedirectResponse from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID from fastapi_sso.sso.base import OpenID, SSOBase
from fastapi_sso.sso.github import GithubSSO
from fastapi_sso.sso.google import GoogleSSO
from loguru import logger from loguru import logger
from starlette.status import ( from starlette.status import (
HTTP_400_BAD_REQUEST, HTTP_400_BAD_REQUEST,
@@ -94,72 +93,37 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.") raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.")
@auth_router.get("/api/v1/auth/google", description="Google SSO") @auth_router.get("/api/v1/auth/{provider}", description="SSO Provider")
async def login_with_google(request: Request, user_id: Optional[str] = None): async def login_with_sso_provider(
google_sso = _new_google_sso() request: Request, provider: str, user_id: Optional[str] = None
if not google_sso: ):
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'Google' not allowed.") provider_sso = _new_sso(provider)
if not provider_sso:
google_sso.redirect_uri = str(request.base_url) + "api/v1/auth/google/token"
with google_sso:
state = encrypt_internal_message(user_id)
return await google_sso.get_login_redirect(state=state)
@auth_router.get("/api/v1/auth/github", description="Github SSO")
async def login_with_github(request: Request, user_id: Optional[str] = None):
github_sso = _new_github_sso()
if not github_sso:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'GitHub' not allowed.")
github_sso.redirect_uri = str(request.base_url) + "api/v1/auth/github/token"
with github_sso:
state = decrypt_internal_message(user_id)
return await github_sso.get_login_redirect(state=state)
@auth_router.get(
"/api/v1/auth/google/token", description="Handle Google OAuth callback"
)
async def handle_google_token(request: Request) -> RedirectResponse:
google_sso = _new_google_sso()
if not google_sso:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'Google' not allowed.")
try:
with google_sso:
userinfo = await google_sso.verify_and_process(request)
assert userinfo is not None
user_id = decrypt_internal_message(google_sso.state)
request.session.pop("user", None)
return await _handle_sso_login(userinfo, user_id)
except HTTPException as e:
raise e
except ValueError as e:
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
except Exception as e:
logger.debug(e)
raise HTTPException( raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot authenticate user with Google Auth." HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed."
) )
provider_sso.redirect_uri = str(request.base_url) + f"api/v1/auth/{provider}/token"
with provider_sso:
state = encrypt_internal_message(user_id)
return await provider_sso.get_login_redirect(state=state)
@auth_router.get(
"/api/v1/auth/github/token", description="Handle Github OAuth callback" @auth_router.get("/api/v1/auth/{provider}/token", description="Handle OAuth callback")
) async def handle_oauth_token(request: Request, provider: str) -> RedirectResponse:
async def handle_github_token(request: Request) -> RedirectResponse: provider_sso = _new_sso(provider)
github_sso = _new_github_sso() if not provider_sso:
if not github_sso: raise HTTPException(
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'GitHub' not allowed.") HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed."
)
try: try:
with github_sso: with provider_sso:
userinfo = await github_sso.verify_and_process(request) userinfo = await provider_sso.verify_and_process(request)
assert userinfo is not None assert userinfo is not None
user_id = decrypt_internal_message(github_sso.state) user_id = decrypt_internal_message(provider_sso.state)
request.session.pop("user", None) request.session.pop("user", None)
return await _handle_sso_login(userinfo, user_id) return await _handle_sso_login(userinfo, user_id)
except HTTPException as e: except HTTPException as e:
raise e raise e
except ValueError as e: except ValueError as e:
@@ -167,7 +131,8 @@ async def handle_github_token(request: Request) -> RedirectResponse:
except Exception as e: except Exception as e:
logger.debug(e) logger.debug(e)
raise HTTPException( raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot authenticate user with GitHub Auth." HTTP_500_INTERNAL_SERVER_ERROR,
f"Cannot authenticate user with {provider} Auth.",
) )
@@ -340,29 +305,44 @@ def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
return response return response
def _new_google_sso() -> Optional[GoogleSSO]: def _new_sso(provider: str) -> Optional[SSOBase]:
if not settings.is_auth_method_allowed(AuthMethods.google_auth): try:
return None if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
if not settings.is_google_auth_configured: return None
logger.warning("Google Auth allowed but not configured.")
return None client_id = getattr(settings, f"{provider}_client_id", None)
return GoogleSSO( client_secret = getattr(settings, f"{provider}_client_secret", None)
settings.google_client_id, discovery_url = getattr(settings, f"{provider}_discovery_url", None)
settings.google_client_secret,
None, if not client_id or not client_secret:
allow_insecure_http=True, logger.warning(f"{provider} auth allowed but not configured.")
) return None
SSOProviderClass = _find_auth_provider_class(provider)
ssoProvider = SSOProviderClass(
client_id, client_secret, None, allow_insecure_http=True
)
if (
discovery_url
and getattr(ssoProvider, "discovery_url", discovery_url) != discovery_url
):
ssoProvider.discovery_url = discovery_url
return ssoProvider
except Exception as e:
logger.warning(e)
return None
def _new_github_sso() -> Optional[GithubSSO]: def _find_auth_provider_class(provider: str) -> Callable:
if not settings.is_auth_method_allowed(AuthMethods.github_auth): sso_modules = ["lnbits.core.sso", "fastapi_sso.sso"]
return None for module in sso_modules:
if not settings.is_github_auth_configured: try:
logger.warning("Github Auth allowed but not configured.") provider_module = importlib.import_module(f"{module}.{provider}")
return None ProviderClass = getattr(provider_module, f"{provider.title()}SSO")
return GithubSSO( if ProviderClass:
settings.github_client_id, return ProviderClass
settings.github_client_secret, except Exception:
None, pass
allow_insecure_http=True,
) raise ValueError(f"No SSO provider found for '{provider}'.")
+3 -4
View File
@@ -95,11 +95,10 @@ async def extensions_install(
installed_exts: List["InstallableExtension"] = await get_installed_extensions() installed_exts: List["InstallableExtension"] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts] installed_exts_ids = [e.id for e in installed_exts]
installable_exts: List[ installable_exts = await InstallableExtension.get_installable_extensions()
InstallableExtension installable_exts_ids = [e.id for e in installable_exts]
] = await InstallableExtension.get_installable_extensions()
installable_exts += [ installable_exts += [
e for e in installed_exts if e.id not in installed_exts_ids e for e in installed_exts if e.id not in installable_exts_ids
] ]
for e in installable_exts: for e in installable_exts:
+1 -1
View File
@@ -369,7 +369,7 @@ class LndRestNode(Node):
memo=invoice["memo"], memo=invoice["memo"],
pending=invoice["state"] == "OPEN", pending=invoice["state"] == "OPEN",
paid_at=invoice["settle_date"], paid_at=invoice["settle_date"],
expiry=invoice["creation_date"] + invoice["expiry"], expiry=int(invoice["creation_date"]) + int(invoice["expiry"]),
preimage=_decode_bytes(invoice["r_preimage"]), preimage=_decode_bytes(invoice["r_preimage"]),
bolt11=invoice["payment_request"], bolt11=invoice["payment_request"],
) )
+17 -7
View File
@@ -116,6 +116,9 @@ class SecuritySettings(LNbitsSettings):
lnbits_notifications: bool = Field(default=False) lnbits_notifications: bool = Field(default=False)
lnbits_killswitch: bool = Field(default=False) lnbits_killswitch: bool = Field(default=False)
lnbits_killswitch_interval: int = Field(default=60) lnbits_killswitch_interval: int = Field(default=60)
lnbits_wallet_limit_max_balance: int = Field(default=0)
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
lnbits_watchdog: bool = Field(default=False) lnbits_watchdog: bool = Field(default=False)
lnbits_watchdog_interval: int = Field(default=60) lnbits_watchdog_interval: int = Field(default=60)
lnbits_watchdog_delta: int = Field(default=1_000_000) lnbits_watchdog_delta: int = Field(default=1_000_000)
@@ -125,6 +128,13 @@ class SecuritySettings(LNbitsSettings):
) )
) )
def is_wallet_max_balance_exceeded(self, amount):
return (
self.lnbits_wallet_limit_max_balance
and self.lnbits_wallet_limit_max_balance > 0
and amount > self.lnbits_wallet_limit_max_balance
)
class FakeWalletFundingSource(LNbitsSettings): class FakeWalletFundingSource(LNbitsSettings):
fake_wallet_secret: str = Field(default="ToTheMoon1") fake_wallet_secret: str = Field(default="ToTheMoon1")
@@ -252,6 +262,7 @@ class AuthMethods(Enum):
username_and_password = "username-password" username_and_password = "username-password"
google_auth = "google-auth" google_auth = "google-auth"
github_auth = "github-auth" github_auth = "github-auth"
keycloak_auth = "keycloak-auth"
class AuthSettings(LNbitsSettings): class AuthSettings(LNbitsSettings):
@@ -272,18 +283,16 @@ class GoogleAuthSettings(LNbitsSettings):
google_client_id: str = Field(default="") google_client_id: str = Field(default="")
google_client_secret: str = Field(default="") google_client_secret: str = Field(default="")
@property
def is_google_auth_configured(self):
return self.google_client_id != "" and self.google_client_secret != ""
class GitHubAuthSettings(LNbitsSettings): class GitHubAuthSettings(LNbitsSettings):
github_client_id: str = Field(default="") github_client_id: str = Field(default="")
github_client_secret: str = Field(default="") github_client_secret: str = Field(default="")
@property
def is_github_auth_configured(self): class KeycloakAuthSettings(LNbitsSettings):
return self.github_client_id != "" and self.github_client_secret != "" keycloak_discovery_url: str = Field(default="")
keycloak_client_id: str = Field(default="")
keycloak_client_secret: str = Field(default="")
class EditableSettings( class EditableSettings(
@@ -299,6 +308,7 @@ class EditableSettings(
AuthSettings, AuthSettings,
GoogleAuthSettings, GoogleAuthSettings,
GitHubAuthSettings, GitHubAuthSettings,
KeycloakAuthSettings,
): ):
@validator( @validator(
"lnbits_admin_users", "lnbits_admin_users",
+9 -9
View File
File diff suppressed because one or more lines are too long
+8
View File
@@ -59,6 +59,7 @@ window.localisation.en = {
service_fee_tooltip: service_fee_tooltip:
'Service fee charged by the LNbits server admin per outgoing transaction', 'Service fee charged by the LNbits server admin per outgoing transaction',
toggle_darkmode: 'Toggle Dark Mode', toggle_darkmode: 'Toggle Dark Mode',
payment_reactions: 'Payment Reactions',
view_swagger_docs: 'View LNbits Swagger API docs', view_swagger_docs: 'View LNbits Swagger API docs',
api_docs: 'API docs', api_docs: 'API docs',
api_keys_api_docs: 'API keys and API docs', api_keys_api_docs: 'API keys and API docs',
@@ -184,6 +185,12 @@ window.localisation.en = {
allow_access_hint: 'Allow access by IP (will override blocked IPs)', allow_access_hint: 'Allow access by IP (will override blocked IPs)',
enter_ip: 'Enter IP and hit enter', enter_ip: 'Enter IP and hit enter',
rate_limiter: 'Rate Limiter', rate_limiter: 'Rate Limiter',
wallet_limiter: 'Wallet Limiter',
wallet_limit_max_withdraw_per_day:
'Max daily wallet withdrawal in sats (0 to disable)',
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
wallet_limit_secs_between_trans:
'Min secs between transactions per wallet (0 to disable)',
number_of_requests: 'Number of requests', number_of_requests: 'Number of requests',
time_unit: 'Time unit', time_unit: 'Time unit',
minute: 'minute', minute: 'minute',
@@ -202,6 +209,7 @@ window.localisation.en = {
account_settings: 'Account Settings', account_settings: 'Account Settings',
signin_with_google: 'Sign in with Google', signin_with_google: 'Sign in with Google',
signin_with_github: 'Sign in with GitHub', signin_with_github: 'Sign in with GitHub',
signin_with_keycloak: 'Sign in with Keycloak',
username_or_email: 'Username or Email', username_or_email: 'Username or Email',
password: 'Password', password: 'Password',
password_config: 'Password Config', password_config: 'Password Config',
+4 -3
View File
@@ -37,7 +37,7 @@ window.localisation.fi = {
paste_invoice_label: paste_invoice_label:
'Liitä lasku, maksupyyntö, lnurl-koodi tai Lightning Address *', 'Liitä lasku, maksupyyntö, lnurl-koodi tai Lightning Address *',
lnbits_description: lnbits_description:
'Kevyt ja helppokäyttöinen LNbits soveltuu minkä tahansa salamaverkon rahoituslähteeksi. Se tukee rahoituslähteinä LND, Core Lightning, OpenNode, Alby, LNPay ja jopa itseään! Voi käyttää itsenäisesti sitä ja helposti tarjota säilytyspalveluita. Pystyt luomaan sillä salamaverkko lompakoita ja niiden määrää ei ole rajoitettu. Jokaiselle lompakolle saat iksilölliset AP avaimet. Varojen osittaminen tekee siitä erittäin kätevän varojen hallinnassa sekä myös ohjelmistokehityksen työkaluja. Laajennukset lisäävät LNbits:in toiminnallisuuksia. Näinpä voit helposti testailla useita erilaisia ja viimeisimpiä salamaverkon-teknologioita. Laajennuksien kehittämisen olemme pyrkineet tekemään hmahdollisimman helpoksi, ilmaisena OpenSource-projektina. Kannustamme kaikkia kehittämään ja jakelemaan omia laajennuksia!', 'Kevyt ja helppokäyttöinen LNbits voi käyttää rahoituslähteinään erilaisia palveluita, kuten LND, Core Lightning, OpenNode, Alby, LNPay ja jopa itseään! Voit käyttää sitä itsenäisesti ja helposti tarjota erilaisia Lightning-palveluita. Pystyt luomaan sillä salamaverkkolompakoita eikä niiden määrää ole rajoitettu. Jokaiselle lompakolle saat yksilölliset API-avaimet. Varojen osittaminen tekee siitä erittäin kätevän varojen hallinnassa sekä myös ohjelmistokehityksen työkalun. Laajennukset lisäävät LNbits:in toiminnallisuuksia. Näinpä voit helposti testailla useita erilaisia ja viimeisimpiä salamaverkon teknologioita. Laajennuksien kehittämisen olemme pyrkineet tekemään mahdollisimman helpoksi pitämällä LNbits:in ilmaisena OpenSource-projektina. Kannustamme kaikkia kehittämään ja jakelemaan omia laajennuksia!',
export_to_phone: 'Käytä puhelimessa lukemalla QR-koodi', export_to_phone: 'Käytä puhelimessa lukemalla QR-koodi',
export_to_phone_desc: export_to_phone_desc:
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voi lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ', 'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voi lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
@@ -64,6 +64,7 @@ window.localisation.fi = {
service_fee_tooltip: service_fee_tooltip:
'LNBits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.', 'LNBits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.',
toggle_darkmode: 'Tumma näkymä', toggle_darkmode: 'Tumma näkymä',
toggle_reactions: 'Käytä tapahtuma efektejä',
view_swagger_docs: 'Näytä LNbits Swagger API-dokumentit', view_swagger_docs: 'Näytä LNbits Swagger API-dokumentit',
api_docs: 'API-dokumentaatio', api_docs: 'API-dokumentaatio',
api_keys_api_docs: 'API-avaimet ja -dokumentaatio', api_keys_api_docs: 'API-avaimet ja -dokumentaatio',
@@ -104,9 +105,9 @@ window.localisation.fi = {
disclaimer_dialog: disclaimer_dialog:
'Muistathan tallettaa kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossakin kirjautumaan lompakkoosi! Tutustu myös Tilin asetukset -sivuun. Tämä palvelu on kokeiluvaiheessa (eli BETA), ja niinpä kukaan ei ota mitään vastuuta varojen säilymisestä tai niiden käytettävyyden takaamisesta.', 'Muistathan tallettaa kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossakin kirjautumaan lompakkoosi! Tutustu myös Tilin asetukset -sivuun. Tämä palvelu on kokeiluvaiheessa (eli BETA), ja niinpä kukaan ei ota mitään vastuuta varojen säilymisestä tai niiden käytettävyyden takaamisesta.',
no_transactions: 'Lompakossa ei ole yhtään tapahtumaa', no_transactions: 'Lompakossa ei ole yhtään tapahtumaa',
manage: 'Hallinnoi', manage: 'Hallinnointi',
extensions: 'Laajennukset', extensions: 'Laajennukset',
no_extensions: 'Yhtään laajennusta ei ole asennettuna :(', no_extensions: 'Laajennuksia ei ole asennettu :(',
created: 'Luotu', created: 'Luotu',
search_extensions: 'Etsi laajennuksia', search_extensions: 'Etsi laajennuksia',
warning: 'Varoitus', warning: 'Varoitus',
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+9
View File
@@ -6,6 +6,12 @@ new Vue({
user: null, user: null,
hasUsername: false, hasUsername: false,
showUserId: false, showUserId: false,
reactionOptions: [
'None',
'confettiBothSides',
'confettiFireworks',
'confettiStars'
],
tab: 'user', tab: 'user',
passwordData: { passwordData: {
show: false, show: false,
@@ -27,6 +33,9 @@ new Vue({
this.$q.dark.toggle() this.$q.dark.toggle()
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive) this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
}, },
reactionChoiceFunc: function () {
this.$q.localStorage.set('lnbits.reactions', this.reactionChoice)
},
changeColor: function (newValue) { changeColor: function (newValue) {
document.body.setAttribute('data-theme', newValue) document.body.setAttribute('data-theme', newValue)
this.$q.localStorage.set('lnbits.theme', newValue) this.$q.localStorage.set('lnbits.theme', newValue)
+82 -1
View File
@@ -48,7 +48,8 @@ new Vue({
show: false show: false
}, },
tab: 'funding', tab: 'funding',
needsRestart: false needsRestart: false,
introJs: null
} }
}, },
created() { created() {
@@ -56,6 +57,32 @@ new Vue({
this.getAudit() this.getAudit()
this.balance = +'{{ balance|safe }}' this.balance = +'{{ balance|safe }}'
}, },
mounted() {
const url = window.location.href
const queryString = url.split('?')[1]
const queryObject = {}
if (queryString) {
for (const param of queryString.split('&')) {
const [key, value] = param.split('=')
queryObject[key] = value
}
}
if (queryObject.hasOwnProperty('first_use')) {
const scriptTag = document.createElement('script')
scriptTag.src = 'https://unpkg.com/intro.js/intro.js'
const linkTag = document.createElement('link')
linkTag.rel = 'stylesheet'
linkTag.href = 'https://unpkg.com/intro.js/introjs.css'
document.body.appendChild(scriptTag)
document.head.appendChild(linkTag)
scriptTag.onload = () => {
this.runOnboarding()
}
}
},
computed: { computed: {
lnbitsVersion() { lnbitsVersion() {
return LNBITS_VERSION return LNBITS_VERSION
@@ -68,6 +95,60 @@ new Vue({
} }
}, },
methods: { methods: {
runOnboarding() {
introJs()
.onbeforeexit(() => {
return window.history.replaceState(
null,
null,
window.location.pathname
)
})
.setOptions({
disableInteraction: true,
tooltipClass: 'introjs-tooltip-custom',
dontShowAgain: true,
steps: [
{
title: 'Welcome to LNbits',
intro: 'Start your tour!'
},
{
title: 'Add a funding source',
element: document.getElementById('funding-sources'),
intro:
'Select a Lightning Network funding source to add funds to your LNbits.'
},
{
title: 'Access your wallet',
element: document.getElementById('wallet-list'),
position: 'right',
intro:
'Wallets are the core of LNbits. They are the place where you can store your funds.'
},
{
title: 'Manage LNbits',
element: document.getElementById('admin-manage'),
position: 'right',
intro:
'This is the place where you can manage your LNbits. You can configure settings, install extensions, themes, view logs, manage a node, and more.'
},
{
title: 'Access extensions',
element: document.getElementById('extension-list'),
position: 'right',
intro:
'Extensions are the way to add functionality to your LNbits. You view and access them here.'
},
{
title: 'Farewell!',
intro: '<img src="static/images/logos/lnbits.svg" width=100%/>'
}
]
})
.start()
},
addAdminUser() { addAdminUser() {
let addUser = this.formAddAdmin let addUser = this.formAddAdmin
let admin_users = this.formData.lnbits_admin_users let admin_users = this.formData.lnbits_admin_users
+4 -1
View File
@@ -168,7 +168,6 @@ window.LNbits = {
let listener = ev => { let listener = ev => {
cb(JSON.parse(ev.data)) cb(JSON.parse(ev.data))
} }
this.listenersCount = this.listenersCount || {[wallet.inkey]: 0} this.listenersCount = this.listenersCount || {[wallet.inkey]: 0}
this.listenersCount[wallet.inkey]++ this.listenersCount[wallet.inkey]++
@@ -444,6 +443,7 @@ window.windowMixin = {
data: function () { data: function () {
return { return {
toggleSubs: true, toggleSubs: true,
reactionChoice: 'confettiBothSides',
isUserAuthorized: false, isUserAuthorized: false,
g: { g: {
offline: !navigator.onLine, offline: !navigator.onLine,
@@ -523,6 +523,9 @@ window.windowMixin = {
} else { } else {
this.$q.dark.set(true) this.$q.dark.set(true)
} }
this.reactionChoice =
this.$q.localStorage.getItem('lnbits.reactions') || 'confettiBothSides'
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin'] this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
let locale = this.$q.localStorage.getItem('lnbits.lang') let locale = this.$q.localStorage.getItem('lnbits.lang')
+560
View File
@@ -0,0 +1,560 @@
function eventReactionWebocket(event_id) {
localUrl = ''
reaction = localStorage.getItem('lnbits.reactions')
if (!reaction || reaction === 'None') {
return
}
if (location.protocol !== 'http:') {
localUrl = 'wss://' + location.host + '/api/v1/ws/' + event_id
} else {
localUrl = 'ws://' + location.host + '/api/v1/ws/' + event_id
}
connection = new WebSocket(localUrl)
connection.onmessage = function (e) {
try {
const parsedData = JSON.parse(e.data)
if (parsedData.payment.amount < 0) {
return
}
reaction = localStorage.getItem('lnbits.reactions')
if (reaction) {
window[reaction.split('|')[1]]()
}
} catch (e) {
console.log(e)
}
}
}
function confettiBothSides() {
document.getElementById('vue').disabled = true
var end = Date.now() + 2 * 1000
var colors = ['#FFD700', '#ffffff']
function frame() {
confetti({
particleCount: 2,
angle: 60,
spread: 55,
origin: {x: 0},
colors: colors,
zIndex: 999999
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
origin: {x: 1},
colors: colors,
zIndex: 999999
})
if (Date.now() < end) {
requestAnimationFrame(frame)
} else {
document.getElementById('vue').disabled = false
}
}
frame()
}
function confettiFireworks() {
var duration = 3 * 1000
var animationEnd = Date.now() + duration
var defaults = {startVelocity: 30, spread: 360, ticks: 60, zIndex: 0}
function randomInRange(min, max) {
return Math.random() * (max - min) + min
}
var interval = setInterval(function () {
var timeLeft = animationEnd - Date.now()
if (timeLeft <= 0) {
return clearInterval(interval)
}
var particleCount = 5 * (timeLeft / duration)
// since particles fall down, start a bit higher than random
confetti({
...defaults,
particleCount,
origin: {x: randomInRange(0.1, 0.3), y: Math.random() - 0.2}
})
confetti({
...defaults,
particleCount,
origin: {x: randomInRange(0.7, 0.9), y: Math.random() - 0.2}
})
}, 250)
}
function confettiStars() {
var defaults = {
spread: 360,
ticks: 50,
gravity: 0,
decay: 0.94,
startVelocity: 30,
colors: ['FFE400', 'FFBD00', 'E89400', 'FFCA6C', 'FDFFB8']
}
function shoot() {
confetti({
...defaults,
particleCount: 40,
scalar: 1.2,
shapes: ['star']
})
confetti({
...defaults,
particleCount: 10,
scalar: 0.75,
shapes: ['circle']
})
}
setTimeout(shoot, 0)
setTimeout(shoot, 100)
setTimeout(shoot, 200)
setTimeout(shoot, 0)
setTimeout(shoot, 100)
setTimeout(shoot, 200)
}
!(function (t, e) {
!(function t(e, n, a, i) {
var o = !!(
e.Worker &&
e.Blob &&
e.Promise &&
e.OffscreenCanvas &&
e.OffscreenCanvasRenderingContext2D &&
e.HTMLCanvasElement &&
e.HTMLCanvasElement.prototype.transferControlToOffscreen &&
e.URL &&
e.URL.createObjectURL
)
function r() {}
function l(t) {
var a = n.exports.Promise,
i = void 0 !== a ? a : e.Promise
return 'function' == typeof i ? new i(t) : (t(r, r), null)
}
var c,
s,
u,
d,
f,
h,
g,
m,
b =
((u = Math.floor(1e3 / 60)),
(d = {}),
(f = 0),
'function' == typeof requestAnimationFrame &&
'function' == typeof cancelAnimationFrame
? ((c = function (t) {
var e = Math.random()
return (
(d[e] = requestAnimationFrame(function n(a) {
f === a || f + u - 1 < a
? ((f = a), delete d[e], t())
: (d[e] = requestAnimationFrame(n))
})),
e
)
}),
(s = function (t) {
d[t] && cancelAnimationFrame(d[t])
}))
: ((c = function (t) {
return setTimeout(t, u)
}),
(s = function (t) {
return clearTimeout(t)
})),
{frame: c, cancel: s}),
v =
((m = {}),
function () {
if (h) return h
if (!a && o) {
var e = [
'var CONFETTI, SIZE = {}, module = {};',
'(' + t.toString() + ')(this, module, true, SIZE);',
'onmessage = function(msg) {',
' if (msg.data.options) {',
' CONFETTI(msg.data.options).then(function () {',
' if (msg.data.callback) {',
' postMessage({ callback: msg.data.callback });',
' }',
' });',
' } else if (msg.data.reset) {',
' CONFETTI.reset();',
' } else if (msg.data.resize) {',
' SIZE.width = msg.data.resize.width;',
' SIZE.height = msg.data.resize.height;',
' } else if (msg.data.canvas) {',
' SIZE.width = msg.data.canvas.width;',
' SIZE.height = msg.data.canvas.height;',
' CONFETTI = module.exports.create(msg.data.canvas);',
' }',
'}'
].join('\n')
try {
h = new Worker(URL.createObjectURL(new Blob([e])))
} catch (t) {
return (
void 0 !== typeof console &&
'function' == typeof console.warn &&
console.warn('🎊 Could not load worker', t),
null
)
}
!(function (t) {
function e(e, n) {
t.postMessage({options: e || {}, callback: n})
}
;(t.init = function (e) {
var n = e.transferControlToOffscreen()
t.postMessage({canvas: n}, [n])
}),
(t.fire = function (n, a, i) {
if (g) return e(n, null), g
var o = Math.random().toString(36).slice(2)
return (g = l(function (a) {
function r(e) {
e.data.callback === o &&
(delete m[o],
t.removeEventListener('message', r),
(g = null),
i(),
a())
}
t.addEventListener('message', r),
e(n, o),
(m[o] = r.bind(null, {data: {callback: o}}))
}))
}),
(t.reset = function () {
for (var e in (t.postMessage({reset: !0}), m))
m[e](), delete m[e]
})
})(h)
}
return h
}),
y = {
particleCount: 50,
angle: 90,
spread: 45,
startVelocity: 45,
decay: 0.9,
gravity: 1,
drift: 0,
ticks: 200,
x: 0.5,
y: 0.5,
shapes: ['square', 'circle'],
zIndex: 100,
colors: [
'#26ccff',
'#a25afd',
'#ff5e7e',
'#88ff5a',
'#fcff42',
'#ffa62d',
'#ff36ff'
],
disableForReducedMotion: !1,
scalar: 1
}
function p(t, e, n) {
return (function (t, e) {
return e ? e(t) : t
})(t && null != t[e] ? t[e] : y[e], n)
}
function M(t) {
return t < 0 ? 0 : Math.floor(t)
}
function w(t) {
return parseInt(t, 16)
}
function x(t) {
return t.map(C)
}
function C(t) {
var e = String(t).replace(/[^0-9a-f]/gi, '')
return (
e.length < 6 && (e = e[0] + e[0] + e[1] + e[1] + e[2] + e[2]),
{
r: w(e.substring(0, 2)),
g: w(e.substring(2, 4)),
b: w(e.substring(4, 6))
}
)
}
function k(t) {
;(t.width = document.documentElement.clientWidth),
(t.height = document.documentElement.clientHeight)
}
function I(t) {
var e = t.getBoundingClientRect()
;(t.width = e.width), (t.height = e.height)
}
function T(t, e, n, o, r) {
var c,
s,
u = e.slice(),
d = t.getContext('2d'),
f = l(function (e) {
function l() {
;(c = s = null), d.clearRect(0, 0, o.width, o.height), r(), e()
}
;(c = b.frame(function e() {
!a ||
(o.width === i.width && o.height === i.height) ||
((o.width = t.width = i.width), (o.height = t.height = i.height)),
o.width ||
o.height ||
(n(t), (o.width = t.width), (o.height = t.height)),
d.clearRect(0, 0, o.width, o.height),
(u = u.filter(function (t) {
return (function (t, e) {
;(e.x += Math.cos(e.angle2D) * e.velocity + e.drift),
(e.y += Math.sin(e.angle2D) * e.velocity + e.gravity),
(e.wobble += 0.1),
(e.velocity *= e.decay),
(e.tiltAngle += 0.1),
(e.tiltSin = Math.sin(e.tiltAngle)),
(e.tiltCos = Math.cos(e.tiltAngle)),
(e.random = Math.random() + 5),
(e.wobbleX = e.x + 10 * e.scalar * Math.cos(e.wobble)),
(e.wobbleY = e.y + 10 * e.scalar * Math.sin(e.wobble))
var n = e.tick++ / e.totalTicks,
a = e.x + e.random * e.tiltCos,
i = e.y + e.random * e.tiltSin,
o = e.wobbleX + e.random * e.tiltCos,
r = e.wobbleY + e.random * e.tiltSin
return (
(t.fillStyle =
'rgba(' +
e.color.r +
', ' +
e.color.g +
', ' +
e.color.b +
', ' +
(1 - n) +
')'),
t.beginPath(),
'circle' === e.shape
? t.ellipse
? t.ellipse(
e.x,
e.y,
Math.abs(o - a) * e.ovalScalar,
Math.abs(r - i) * e.ovalScalar,
(Math.PI / 10) * e.wobble,
0,
2 * Math.PI
)
: (function (t, e, n, a, i, o, r, l, c) {
t.save(),
t.translate(e, n),
t.rotate(o),
t.scale(a, i),
t.arc(0, 0, 1, r, l, c),
t.restore()
})(
t,
e.x,
e.y,
Math.abs(o - a) * e.ovalScalar,
Math.abs(r - i) * e.ovalScalar,
(Math.PI / 10) * e.wobble,
0,
2 * Math.PI
)
: (t.moveTo(Math.floor(e.x), Math.floor(e.y)),
t.lineTo(Math.floor(e.wobbleX), Math.floor(i)),
t.lineTo(Math.floor(o), Math.floor(r)),
t.lineTo(Math.floor(a), Math.floor(e.wobbleY))),
t.closePath(),
t.fill(),
e.tick < e.totalTicks
)
})(d, t)
})).length
? (c = b.frame(e))
: l()
})),
(s = l)
})
return {
addFettis: function (t) {
return (u = u.concat(t)), f
},
canvas: t,
promise: f,
reset: function () {
c && b.cancel(c), s && s()
}
}
}
function E(t, n) {
var a,
i = !t,
r = !!p(n || {}, 'resize'),
c = p(n, 'disableForReducedMotion', Boolean),
s = o && !!p(n || {}, 'useWorker') ? v() : null,
u = i ? k : I,
d = !(!t || !s) && !!t.__confetti_initialized,
f =
'function' == typeof matchMedia &&
matchMedia('(prefers-reduced-motion)').matches
function h(e, n, i) {
for (
var o,
r,
l,
c,
s,
d = p(e, 'particleCount', M),
f = p(e, 'angle', Number),
h = p(e, 'spread', Number),
g = p(e, 'startVelocity', Number),
m = p(e, 'decay', Number),
b = p(e, 'gravity', Number),
v = p(e, 'drift', Number),
y = p(e, 'colors', x),
w = p(e, 'ticks', Number),
C = p(e, 'shapes'),
k = p(e, 'scalar'),
I = (function (t) {
var e = p(t, 'origin', Object)
return (e.x = p(e, 'x', Number)), (e.y = p(e, 'y', Number)), e
})(e),
E = d,
S = [],
F = t.width * I.x,
N = t.height * I.y;
E--;
)
S.push(
((o = {
x: F,
y: N,
angle: f,
spread: h,
startVelocity: g,
color: y[E % y.length],
shape:
C[
((c = 0),
(s = C.length),
Math.floor(Math.random() * (s - c)) + c)
],
ticks: w,
decay: m,
gravity: b,
drift: v,
scalar: k
}),
(r = void 0),
(l = void 0),
(r = o.angle * (Math.PI / 180)),
(l = o.spread * (Math.PI / 180)),
{
x: o.x,
y: o.y,
wobble: 10 * Math.random(),
velocity: 0.5 * o.startVelocity + Math.random() * o.startVelocity,
angle2D: -r + (0.5 * l - Math.random() * l),
tiltAngle: Math.random() * Math.PI,
color: o.color,
shape: o.shape,
tick: 0,
totalTicks: o.ticks,
decay: o.decay,
drift: o.drift,
random: Math.random() + 5,
tiltSin: 0,
tiltCos: 0,
wobbleX: 0,
wobbleY: 0,
gravity: 3 * o.gravity,
ovalScalar: 0.6,
scalar: o.scalar
})
)
return a ? a.addFettis(S) : (a = T(t, S, u, n, i)).promise
}
function g(n) {
var o = c || p(n, 'disableForReducedMotion', Boolean),
g = p(n, 'zIndex', Number)
if (o && f)
return l(function (t) {
t()
})
i && a
? (t = a.canvas)
: i &&
!t &&
((t = (function (t) {
var e = document.createElement('canvas')
return (
(e.style.position = 'fixed'),
(e.style.top = '0px'),
(e.style.left = '0px'),
(e.style.pointerEvents = 'none'),
(e.style.zIndex = t),
e
)
})(g)),
document.body.appendChild(t)),
r && !d && u(t)
var m = {width: t.width, height: t.height}
function b() {
if (s) {
var e = {
getBoundingClientRect: function () {
if (!i) return t.getBoundingClientRect()
}
}
return (
u(e),
void s.postMessage({resize: {width: e.width, height: e.height}})
)
}
m.width = m.height = null
}
function v() {
;(a = null),
r && e.removeEventListener('resize', b),
i && t && (document.body.removeChild(t), (t = null), (d = !1))
}
return (
s && !d && s.init(t),
(d = !0),
s && (t.__confetti_initialized = !0),
r && e.addEventListener('resize', b, !1),
s ? s.fire(n, m, v) : h(n, m, v)
)
}
return (
(g.reset = function () {
s && s.reset(), a && a.reset()
}),
g
)
}
;(n.exports = E(null, {useWorker: !0, resize: !0})), (n.exports.create = E)
})(
(function () {
return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {}
})(),
e,
!1
),
(t.confetti = e.exports)
})(window, {})
+1 -1
View File
@@ -50,7 +50,7 @@ new Vue({
this.password, this.password,
this.passwordRepeat this.passwordRepeat
) )
window.location.href = '/wallet' window.location.href = '/wallet?first_use'
} catch (e) { } catch (e) {
LNbits.utils.notifyApiError(e) LNbits.utils.notifyApiError(e)
} }
+1 -1
View File
@@ -1,6 +1,6 @@
// update cache version every time there is a new deployment // update cache version every time there is a new deployment
// so the service worker reinitializes the cache // so the service worker reinitializes the cache
const CACHE_VERSION = 107 const CACHE_VERSION = 115
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-` const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => { const getApiKey = request => {
+101 -1
View File
@@ -818,6 +818,105 @@ new Vue({
navigator.clipboard.readText().then(text => { navigator.clipboard.readText().then(text => {
this.$refs.textArea.value = text this.$refs.textArea.value = text
}) })
},
checkFirstUse() {
const url = window.location.href
const queryString = url.split('?')[1]
const queryObject = {}
if (queryString) {
for (const param of queryString.split('&')) {
const [key, value] = param.split('=')
queryObject[key] = value
}
}
if (queryObject.hasOwnProperty('first_use')) {
const scriptTag = document.createElement('script')
scriptTag.src = 'https://unpkg.com/intro.js/intro.js'
const linkTag = document.createElement('link')
linkTag.rel = 'stylesheet'
linkTag.href = 'https://unpkg.com/intro.js/introjs.css'
document.body.appendChild(scriptTag)
document.head.appendChild(linkTag)
scriptTag.onload = () => {
if (!this.g.visibleDrawer) {
this.g.visibleDrawer = true
}
this.runOnboarding()
}
}
},
runOnboarding() {
introJs()
.onbeforechange(step => {
if (!step.id || !this.mobileSimple) return
if (step.id === 'wallet-list' || step.id === 'admin-manage') {
this.g.visibleDrawer = true
} else {
this.g.visibleDrawer = false
}
})
// .onbeforeexit(() => {
// return window.history.replaceState(
// null,
// null,
// window.location.pathname
// )
// })
.setOptions({
disableInteraction: true,
tooltipClass: 'introjs-tooltip-custom',
dontShowAgain: true,
steps: [
{
title: 'Welcome to LNbits',
intro: 'Start your tour!'
},
{
title: 'Access your wallet',
element: document.getElementById('wallet-list'),
position: 'right',
intro:
'Wallets are the core of LNbits. They are the place where you can store your funds.'
},
{
title: 'Access extensions',
element: document.getElementById('admin-manage'),
position: 'right',
intro:
'Extensions are the way to add functionality to your LNbits. You view and access them here.'
},
{
title: 'Wallet balance',
element: document.getElementById('wallet-balance'),
position: 'right',
intro: 'Your wallet balance is displayed here.'
},
{
title: 'Send and receive payments',
element: document.getElementById(
this.mobileSimple ? 'mobile-wallet-buttons' : 'wallet-buttons'
),
position: 'bottom',
intro:
'Use these buttons to send and receive payments or scan a QR code.'
},
{
title: 'Transaction details',
element: document.getElementById('wallet-table'),
position: 'bottom',
intro:
'Here you can see all the transactions made in your wallet. You can also export them as a CSV file.'
},
{
title: 'Farewell!',
intro: '<img src="static/images/logos/lnbits.svg" width=100%/>'
}
]
})
.start()
} }
}, },
watch: { watch: {
@@ -860,11 +959,12 @@ new Vue({
this.disclaimerDialog.show = true this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true) this.$q.localStorage.set('lnbits.disclaimerShown', true)
} }
// listen to incoming payments // listen to incoming payments
LNbits.events.onInvoicePaid(this.g.wallet, payment => LNbits.events.onInvoicePaid(this.g.wallet, payment =>
this.onPaymentReceived(payment.payment_hash) this.onPaymentReceived(payment.payment_hash)
) )
eventReactionWebocket(wallet.id)
this.checkFirstUse()
} }
}) })
+1
View File
@@ -37,6 +37,7 @@
"js/components.js", "js/components.js",
"js/components/lnbits-funding-sources.js", "js/components/lnbits-funding-sources.js",
"js/components/extension-settings.js", "js/components/extension-settings.js",
"js/event-reactions.js",
"js/bolt11-decoder.js" "js/bolt11-decoder.js"
], ],
"css": ["vendor/quasar.css", "vendor/Chart.css", "css/base.css"] "css": ["vendor/quasar.css", "vendor/Chart.css", "css/base.css"]
-10
View File
@@ -14,11 +14,9 @@ from pywebpush import WebPushException, webpush
from lnbits.core.crud import ( from lnbits.core.crud import (
delete_expired_invoices, delete_expired_invoices,
delete_webpush_subscriptions, delete_webpush_subscriptions,
get_balance_checks,
get_payments, get_payments,
get_standalone_payment, get_standalone_payment,
) )
from lnbits.core.services import redeem_lnurl_withdraw
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.wallets import get_wallet_class from lnbits.wallets import get_wallet_class
@@ -186,14 +184,6 @@ async def check_pending_payments():
await asyncio.sleep(60 * 30) # every 30 minutes await asyncio.sleep(60 * 30) # every 30 minutes
async def perform_balance_checks():
while True:
for bc in await get_balance_checks():
await redeem_lnurl_withdraw(bc.wallet, bc.url)
await asyncio.sleep(60 * 60 * 6) # every 6 hours
async def invoice_callback_dispatcher(checking_id: str): async def invoice_callback_dispatcher(checking_id: str):
""" """
Takes incoming payments, sets pending=False, and dispatches them to Takes incoming payments, sets pending=False, and dispatches them to
+6 -2
View File
@@ -168,13 +168,17 @@
show-if-above show-if-above
:elevated="$q.screen.lt.md" :elevated="$q.screen.lt.md"
> >
<lnbits-wallet-list></lnbits-wallet-list> <lnbits-wallet-list id="wallet-list"></lnbits-wallet-list>
<lnbits-manage <lnbits-manage
id="admin-manage"
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'" :show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
:show-node="'{{LNBITS_NODE_UI}}' == 'True'" :show-node="'{{LNBITS_NODE_UI}}' == 'True'"
></lnbits-manage> ></lnbits-manage>
<lnbits-extension-list class="q-pb-xl"></lnbits-extension-list> <lnbits-extension-list
id="extension-list"
class="q-pb-xl"
></lnbits-extension-list>
</q-drawer> </q-drawer>
{% endblock %} {% block page_container %} {% endblock %} {% block page_container %}
<q-page-container> <q-page-container>
+345 -755
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -86,6 +86,7 @@
"js/components.js", "js/components.js",
"js/components/lnbits-funding-sources.js", "js/components/lnbits-funding-sources.js",
"js/components/extension-settings.js", "js/components/extension-settings.js",
"js/event-reactions.js",
"js/bolt11-decoder.js" "js/bolt11-decoder.js"
], ],
"css": [ "css": [
Generated
+1 -1
View File
@@ -2862,4 +2862,4 @@ liquid = ["wallycore"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10 | ^3.9" python-versions = "^3.10 | ^3.9"
content-hash = "c29cac992fae552eb8da791e13fe7766eab7571e260a42cc0de0d755508c42b7" content-hash = "b2e21e0075047150888581a401d46fbe8efd6226e85a85189c3f3db51f825a48"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "lnbits" name = "lnbits"
version = "0.12.0" version = "0.12.1"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"] authors = ["Alan Bits <alan@lnbits.com>"]
+3 -3
View File
@@ -36,7 +36,7 @@ is_regtest: bool = not is_fake
docker_lightning_cli = [ docker_lightning_cli = [
"docker", "docker",
"exec", "exec",
"lnbits-legend-lnd-1-1", "lnbits-lnd-1-1",
"lncli", "lncli",
"--network", "--network",
"regtest", "regtest",
@@ -46,7 +46,7 @@ docker_lightning_cli = [
docker_bitcoin_cli = [ docker_bitcoin_cli = [
"docker", "docker",
"exec", "exec",
"lnbits-legend-bitcoind-1-1" "bitcoin-cli", "lnbits-bitcoind-1-1" "bitcoin-cli",
"-rpcuser=lnbits", "-rpcuser=lnbits",
"-rpcpassword=lnbits", "-rpcpassword=lnbits",
"-regtest", "-regtest",
@@ -56,7 +56,7 @@ docker_bitcoin_cli = [
docker_lightning_unconnected_cli = [ docker_lightning_unconnected_cli = [
"docker", "docker",
"exec", "exec",
"lnbits-legend-lnd-2-1", "lnbits-lnd-2-1",
"lncli", "lncli",
"--network", "--network",
"regtest", "regtest",