Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53fee6896 | ||
|
|
f14dfd2522 | ||
|
|
526467747e | ||
|
|
b8d295a5b7 | ||
|
|
293b6267be | ||
|
|
20e70854a1 | ||
|
|
11173e6460 | ||
|
|
069a7922fc | ||
|
|
98338ffb00 | ||
|
|
62dec118af | ||
|
|
84b406534d | ||
|
|
d9d2d59b73 | ||
|
|
20d4b954c0 | ||
|
|
5edb845d8f | ||
|
|
cfe2b43d2a | ||
|
|
a0ac41a9ce | ||
|
|
194a527736 | ||
|
|
6f135c0696 | ||
|
|
9a767b627f | ||
|
|
c1c4eda69d | ||
|
|
6c46867698 | ||
|
|
a73eb569ae |
+40
-24
@@ -1,5 +1,28 @@
|
||||
#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 ############
|
||||
######################################
|
||||
@@ -78,48 +101,34 @@ ECLAIR_PASS=eclairpw
|
||||
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
||||
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 ##########
|
||||
######################################
|
||||
# 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_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"
|
||||
# Set this flag if HTTP is used for OAuth
|
||||
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
||||
|
||||
# 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_SECRET=""
|
||||
|
||||
# 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_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
|
||||
@@ -198,6 +207,13 @@ LNBITS_RESERVE_FEE_MIN=2000
|
||||
# value in percent
|
||||
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
|
||||
# LNBITS_ALLOWED_CURRENCIES="EUR, USD"
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
default: latest
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
@@ -38,7 +43,7 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
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-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
@@ -48,6 +53,6 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
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-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
@@ -33,8 +33,8 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: lnbitsdocker/lnbits-legend:latest
|
||||
cache-from: type=registry,ref=lnbitsdocker/lnbits-legend:latest
|
||||
tags: lnbits/lnbits:latest
|
||||
cache-from: type=registry,ref=lnbits/lnbits:latest
|
||||
cache-to: type=inline
|
||||
|
||||
- name: Setup Regtest
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Create fake admin
|
||||
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
|
||||
env:
|
||||
|
||||
@@ -13,6 +13,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Create github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -101,19 +101,19 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
|
||||
|
||||
use latest version from docker hub
|
||||
```sh
|
||||
docker pull lnbitsdocker/lnbits-legend
|
||||
docker pull lnbits/lnbits
|
||||
wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env
|
||||
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
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
docker build -t lnbitsdocker/lnbits-legend .
|
||||
docker build -t lnbits/lnbits .
|
||||
cp .env.example .env
|
||||
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
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ from slowapi.util import get_remote_address
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
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.services import websocketUpdater
|
||||
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.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.db import core_app_extra
|
||||
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)
|
||||
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)
|
||||
|
||||
# mount routes for the new version
|
||||
|
||||
@@ -1097,6 +1097,8 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
|
||||
return None
|
||||
row_dict = dict(sets)
|
||||
row_dict.pop("super_user")
|
||||
row_dict.pop("auth_all_methods")
|
||||
|
||||
admin_settings = AdminSettings(
|
||||
is_super_user=is_super_user,
|
||||
lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources,
|
||||
|
||||
+67
-2
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, TypedDict
|
||||
@@ -41,6 +42,7 @@ from .crud import (
|
||||
create_wallet,
|
||||
delete_wallet_payment,
|
||||
get_account,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_super_settings,
|
||||
get_total_balance,
|
||||
@@ -118,8 +120,9 @@ async def create_invoice(
|
||||
if not amount > 0:
|
||||
raise InvoiceFailure("Amountless invoices not supported.")
|
||||
|
||||
if await get_wallet(wallet_id, conn=conn) is None:
|
||||
raise InvoiceFailure("Wallet does not exist.")
|
||||
user_wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not user_wallet:
|
||||
raise InvoiceFailure(f"Could not fetch wallet '{wallet_id}'.")
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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(
|
||||
amount=amount_sat,
|
||||
memo=invoice_memo,
|
||||
@@ -185,6 +196,8 @@ async def pay_invoice(
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
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:
|
||||
temp_id = invoice.payment_hash
|
||||
internal_id = f"internal_{invoice.payment_hash}"
|
||||
@@ -372,6 +385,58 @@ async def pay_invoice(
|
||||
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(
|
||||
wallet_id: str,
|
||||
lnurl_request: str,
|
||||
|
||||
@@ -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 v-if="isSuperUser">
|
||||
<div v-if="isSuperUser" id="funding-sources">
|
||||
<lnbits-funding-sources
|
||||
:form-data="formData"
|
||||
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
||||
|
||||
@@ -78,6 +78,41 @@
|
||||
</div>
|
||||
</div>
|
||||
</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-card-section class="q-pa-none">
|
||||
<br />
|
||||
@@ -180,6 +215,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-12">
|
||||
<p v-text="$t('rate_limiter')"></p>
|
||||
<div class="row q-col-gutter-md">
|
||||
@@ -201,6 +237,36 @@
|
||||
</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>
|
||||
<br />
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="user in formData.lnbits_admin_users"
|
||||
:key="user"
|
||||
@@ -24,10 +23,9 @@
|
||||
@remove="removeAdminUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
v-text="user"
|
||||
>
|
||||
{{ user }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
@@ -44,7 +42,6 @@
|
||||
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{% raw %}
|
||||
<q-chip
|
||||
v-for="user in formData.lnbits_allowed_users"
|
||||
:key="user"
|
||||
@@ -52,12 +49,29 @@
|
||||
@remove="removeAllowedUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
v-text="user"
|
||||
>
|
||||
{{ user }}
|
||||
</q-chip>
|
||||
{% endraw %}
|
||||
</div>
|
||||
<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>
|
||||
</q-card-section>
|
||||
|
||||
@@ -165,4 +165,10 @@
|
||||
</q-dialog>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
||||
<style>
|
||||
.introjs-tooltip-custom .introjs-tooltiptext,
|
||||
.introjs-tooltip-header {
|
||||
color: #111;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -401,6 +401,23 @@
|
||||
></lnbits-notifications-btn>
|
||||
</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-panels>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div
|
||||
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 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">
|
||||
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
|
||||
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
|
||||
<h3 class="q-my-none" v-if="'{{SITE_TITLE}}' == 'LNbits'">
|
||||
{{SITE_TITLE}}
|
||||
</h3>
|
||||
<h5 class="q-my-md" v-if="'{{SITE_TITLE}}' == 'LNbits'">
|
||||
{{SITE_TAGLINE}}
|
||||
</h5>
|
||||
</div>
|
||||
{% if lnurl and LNBITS_NEW_ACCOUNTS_ALLOWED and ("user-id-only" in
|
||||
LNBITS_AUTH_METHODS)%}
|
||||
@@ -33,7 +37,7 @@
|
||||
{%else%} {% endif %}
|
||||
|
||||
<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>
|
||||
<div class="text-h5">
|
||||
<span v-text="$t('session_has_expired')"></span>
|
||||
@@ -259,6 +263,25 @@
|
||||
<div><span v-text="$t('signin_with_github')"></span></div>
|
||||
</q-btn>
|
||||
</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%}
|
||||
</div>
|
||||
</q-card-section>
|
||||
@@ -282,11 +305,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ads -->
|
||||
<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="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||
<q-btn
|
||||
@@ -465,27 +493,22 @@
|
||||
<div class="col q-pl-md"></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>
|
||||
{% 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 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-10 q-pl-xl">
|
||||
<span v-text="$t('lnbits_description')"></span>
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user, wallet) }}
|
||||
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
|
||||
<style>
|
||||
.introjs-tooltip-custom .introjs-tooltiptext,
|
||||
.introjs-tooltip-header {
|
||||
color: #111;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
<!---->
|
||||
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
|
||||
@@ -34,7 +40,7 @@
|
||||
} : ''"
|
||||
>
|
||||
<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>
|
||||
<small>{{LNBITS_DENOMINATION}}</small>
|
||||
<lnbits-update-balance
|
||||
@@ -68,7 +74,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<q-btn
|
||||
unelevated
|
||||
@@ -165,6 +174,7 @@
|
||||
:hide-header="mobileSimple"
|
||||
:hide-bottom="mobileSimple"
|
||||
@request="fetchPayments"
|
||||
id="wallet-table"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
@@ -895,6 +905,7 @@
|
||||
</q-dialog>
|
||||
|
||||
<q-tabs
|
||||
id="mobile-wallet-buttons"
|
||||
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
|
||||
active-class="px-0"
|
||||
indicator-color="transparent"
|
||||
|
||||
@@ -17,9 +17,11 @@ from lnbits.core.services import (
|
||||
update_cached_settings,
|
||||
update_wallet_balance,
|
||||
)
|
||||
from lnbits.core.tasks import api_invoice_listeners
|
||||
from lnbits.decorators import check_admin, check_super_user
|
||||
from lnbits.server import server_restart
|
||||
from lnbits.settings import AdminSettings, UpdateSettings, settings
|
||||
from lnbits.tasks import invoice_listeners
|
||||
|
||||
from .. import core_app_extra
|
||||
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])
|
||||
async def api_get_settings(
|
||||
user: User = Depends(check_admin),
|
||||
|
||||
@@ -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.responses import JSONResponse, RedirectResponse
|
||||
from fastapi_sso.sso.base import OpenID
|
||||
from fastapi_sso.sso.github import GithubSSO
|
||||
from fastapi_sso.sso.google import GoogleSSO
|
||||
from fastapi_sso.sso.base import OpenID, SSOBase
|
||||
from loguru import logger
|
||||
from starlette.status import (
|
||||
HTTP_400_BAD_REQUEST,
|
||||
@@ -94,72 +93,37 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.")
|
||||
|
||||
|
||||
@auth_router.get("/api/v1/auth/google", description="Google SSO")
|
||||
async def login_with_google(request: Request, user_id: Optional[str] = None):
|
||||
google_sso = _new_google_sso()
|
||||
if not google_sso:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'Google' not allowed.")
|
||||
|
||||
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)
|
||||
@auth_router.get("/api/v1/auth/{provider}", description="SSO Provider")
|
||||
async def login_with_sso_provider(
|
||||
request: Request, provider: str, user_id: Optional[str] = None
|
||||
):
|
||||
provider_sso = _new_sso(provider)
|
||||
if not provider_sso:
|
||||
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"
|
||||
)
|
||||
async def handle_github_token(request: Request) -> RedirectResponse:
|
||||
github_sso = _new_github_sso()
|
||||
if not github_sso:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'GitHub' not allowed.")
|
||||
|
||||
@auth_router.get("/api/v1/auth/{provider}/token", description="Handle OAuth callback")
|
||||
async def handle_oauth_token(request: Request, provider: str) -> RedirectResponse:
|
||||
provider_sso = _new_sso(provider)
|
||||
if not provider_sso:
|
||||
raise HTTPException(
|
||||
HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed."
|
||||
)
|
||||
|
||||
try:
|
||||
with github_sso:
|
||||
userinfo = await github_sso.verify_and_process(request)
|
||||
with provider_sso:
|
||||
userinfo = await provider_sso.verify_and_process(request)
|
||||
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)
|
||||
return await _handle_sso_login(userinfo, user_id)
|
||||
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except ValueError as e:
|
||||
@@ -167,7 +131,8 @@ async def handle_github_token(request: Request) -> RedirectResponse:
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
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
|
||||
|
||||
|
||||
def _new_google_sso() -> Optional[GoogleSSO]:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.google_auth):
|
||||
return None
|
||||
if not settings.is_google_auth_configured:
|
||||
logger.warning("Google Auth allowed but not configured.")
|
||||
return None
|
||||
return GoogleSSO(
|
||||
settings.google_client_id,
|
||||
settings.google_client_secret,
|
||||
None,
|
||||
allow_insecure_http=True,
|
||||
)
|
||||
def _new_sso(provider: str) -> Optional[SSOBase]:
|
||||
try:
|
||||
if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
|
||||
return None
|
||||
|
||||
client_id = getattr(settings, f"{provider}_client_id", None)
|
||||
client_secret = getattr(settings, f"{provider}_client_secret", None)
|
||||
discovery_url = getattr(settings, f"{provider}_discovery_url", None)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
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]:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.github_auth):
|
||||
return None
|
||||
if not settings.is_github_auth_configured:
|
||||
logger.warning("Github Auth allowed but not configured.")
|
||||
return None
|
||||
return GithubSSO(
|
||||
settings.github_client_id,
|
||||
settings.github_client_secret,
|
||||
None,
|
||||
allow_insecure_http=True,
|
||||
)
|
||||
def _find_auth_provider_class(provider: str) -> Callable:
|
||||
sso_modules = ["lnbits.core.sso", "fastapi_sso.sso"]
|
||||
for module in sso_modules:
|
||||
try:
|
||||
provider_module = importlib.import_module(f"{module}.{provider}")
|
||||
ProviderClass = getattr(provider_module, f"{provider.title()}SSO")
|
||||
if ProviderClass:
|
||||
return ProviderClass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise ValueError(f"No SSO provider found for '{provider}'.")
|
||||
|
||||
@@ -95,11 +95,10 @@ async def extensions_install(
|
||||
installed_exts: List["InstallableExtension"] = await get_installed_extensions()
|
||||
installed_exts_ids = [e.id for e in installed_exts]
|
||||
|
||||
installable_exts: List[
|
||||
InstallableExtension
|
||||
] = await InstallableExtension.get_installable_extensions()
|
||||
installable_exts = await InstallableExtension.get_installable_extensions()
|
||||
installable_exts_ids = [e.id for e in installable_exts]
|
||||
installable_exts += [
|
||||
e for e in installed_exts if e.id not in installed_exts_ids
|
||||
e for e in installed_exts if e.id not in installable_exts_ids
|
||||
]
|
||||
|
||||
for e in installable_exts:
|
||||
|
||||
@@ -369,7 +369,7 @@ class LndRestNode(Node):
|
||||
memo=invoice["memo"],
|
||||
pending=invoice["state"] == "OPEN",
|
||||
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"]),
|
||||
bolt11=invoice["payment_request"],
|
||||
)
|
||||
|
||||
+17
-7
@@ -116,6 +116,9 @@ class SecuritySettings(LNbitsSettings):
|
||||
lnbits_notifications: bool = Field(default=False)
|
||||
lnbits_killswitch: bool = Field(default=False)
|
||||
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_interval: int = Field(default=60)
|
||||
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):
|
||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||
@@ -252,6 +262,7 @@ class AuthMethods(Enum):
|
||||
username_and_password = "username-password"
|
||||
google_auth = "google-auth"
|
||||
github_auth = "github-auth"
|
||||
keycloak_auth = "keycloak-auth"
|
||||
|
||||
|
||||
class AuthSettings(LNbitsSettings):
|
||||
@@ -272,18 +283,16 @@ class GoogleAuthSettings(LNbitsSettings):
|
||||
google_client_id: 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):
|
||||
github_client_id: str = Field(default="")
|
||||
github_client_secret: str = Field(default="")
|
||||
|
||||
@property
|
||||
def is_github_auth_configured(self):
|
||||
return self.github_client_id != "" and self.github_client_secret != ""
|
||||
|
||||
class KeycloakAuthSettings(LNbitsSettings):
|
||||
keycloak_discovery_url: str = Field(default="")
|
||||
keycloak_client_id: str = Field(default="")
|
||||
keycloak_client_secret: str = Field(default="")
|
||||
|
||||
|
||||
class EditableSettings(
|
||||
@@ -299,6 +308,7 @@ class EditableSettings(
|
||||
AuthSettings,
|
||||
GoogleAuthSettings,
|
||||
GitHubAuthSettings,
|
||||
KeycloakAuthSettings,
|
||||
):
|
||||
@validator(
|
||||
"lnbits_admin_users",
|
||||
|
||||
Vendored
+9
-9
File diff suppressed because one or more lines are too long
@@ -59,6 +59,7 @@ window.localisation.en = {
|
||||
service_fee_tooltip:
|
||||
'Service fee charged by the LNbits server admin per outgoing transaction',
|
||||
toggle_darkmode: 'Toggle Dark Mode',
|
||||
payment_reactions: 'Payment Reactions',
|
||||
view_swagger_docs: 'View LNbits Swagger API docs',
|
||||
api_docs: '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)',
|
||||
enter_ip: 'Enter IP and hit enter',
|
||||
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',
|
||||
time_unit: 'Time unit',
|
||||
minute: 'minute',
|
||||
@@ -202,6 +209,7 @@ window.localisation.en = {
|
||||
account_settings: 'Account Settings',
|
||||
signin_with_google: 'Sign in with Google',
|
||||
signin_with_github: 'Sign in with GitHub',
|
||||
signin_with_keycloak: 'Sign in with Keycloak',
|
||||
username_or_email: 'Username or Email',
|
||||
password: 'Password',
|
||||
password_config: 'Password Config',
|
||||
|
||||
@@ -37,7 +37,7 @@ window.localisation.fi = {
|
||||
paste_invoice_label:
|
||||
'Liitä lasku, maksupyyntö, lnurl-koodi tai Lightning Address *',
|
||||
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_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. ',
|
||||
@@ -64,6 +64,7 @@ window.localisation.fi = {
|
||||
service_fee_tooltip:
|
||||
'LNBits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.',
|
||||
toggle_darkmode: 'Tumma näkymä',
|
||||
toggle_reactions: 'Käytä tapahtuma efektejä',
|
||||
view_swagger_docs: 'Näytä LNbits Swagger API-dokumentit',
|
||||
api_docs: 'API-dokumentaatio',
|
||||
api_keys_api_docs: 'API-avaimet ja -dokumentaatio',
|
||||
@@ -104,9 +105,9 @@ window.localisation.fi = {
|
||||
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.',
|
||||
no_transactions: 'Lompakossa ei ole yhtään tapahtumaa',
|
||||
manage: 'Hallinnoi',
|
||||
manage: 'Hallinnointi',
|
||||
extensions: 'Laajennukset',
|
||||
no_extensions: 'Yhtään laajennusta ei ole asennettuna :(',
|
||||
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
||||
created: 'Luotu',
|
||||
search_extensions: 'Etsi laajennuksia',
|
||||
warning: 'Varoitus',
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -6,6 +6,12 @@ new Vue({
|
||||
user: null,
|
||||
hasUsername: false,
|
||||
showUserId: false,
|
||||
reactionOptions: [
|
||||
'None',
|
||||
'confettiBothSides',
|
||||
'confettiFireworks',
|
||||
'confettiStars'
|
||||
],
|
||||
tab: 'user',
|
||||
passwordData: {
|
||||
show: false,
|
||||
@@ -27,6 +33,9 @@ new Vue({
|
||||
this.$q.dark.toggle()
|
||||
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
|
||||
},
|
||||
reactionChoiceFunc: function () {
|
||||
this.$q.localStorage.set('lnbits.reactions', this.reactionChoice)
|
||||
},
|
||||
changeColor: function (newValue) {
|
||||
document.body.setAttribute('data-theme', newValue)
|
||||
this.$q.localStorage.set('lnbits.theme', newValue)
|
||||
|
||||
@@ -48,7 +48,8 @@ new Vue({
|
||||
show: false
|
||||
},
|
||||
tab: 'funding',
|
||||
needsRestart: false
|
||||
needsRestart: false,
|
||||
introJs: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -56,6 +57,32 @@ new Vue({
|
||||
this.getAudit()
|
||||
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: {
|
||||
lnbitsVersion() {
|
||||
return LNBITS_VERSION
|
||||
@@ -68,6 +95,60 @@ new Vue({
|
||||
}
|
||||
},
|
||||
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() {
|
||||
let addUser = this.formAddAdmin
|
||||
let admin_users = this.formData.lnbits_admin_users
|
||||
|
||||
@@ -168,7 +168,6 @@ window.LNbits = {
|
||||
let listener = ev => {
|
||||
cb(JSON.parse(ev.data))
|
||||
}
|
||||
|
||||
this.listenersCount = this.listenersCount || {[wallet.inkey]: 0}
|
||||
this.listenersCount[wallet.inkey]++
|
||||
|
||||
@@ -444,6 +443,7 @@ window.windowMixin = {
|
||||
data: function () {
|
||||
return {
|
||||
toggleSubs: true,
|
||||
reactionChoice: 'confettiBothSides',
|
||||
isUserAuthorized: false,
|
||||
g: {
|
||||
offline: !navigator.onLine,
|
||||
@@ -523,6 +523,9 @@ window.windowMixin = {
|
||||
} else {
|
||||
this.$q.dark.set(true)
|
||||
}
|
||||
this.reactionChoice =
|
||||
this.$q.localStorage.getItem('lnbits.reactions') || 'confettiBothSides'
|
||||
|
||||
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
|
||||
|
||||
let locale = this.$q.localStorage.getItem('lnbits.lang')
|
||||
|
||||
@@ -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, {})
|
||||
@@ -50,7 +50,7 @@ new Vue({
|
||||
this.password,
|
||||
this.passwordRepeat
|
||||
)
|
||||
window.location.href = '/wallet'
|
||||
window.location.href = '/wallet?first_use'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// update cache version every time there is a new deployment
|
||||
// so the service worker reinitializes the cache
|
||||
const CACHE_VERSION = 107
|
||||
const CACHE_VERSION = 115
|
||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||
|
||||
const getApiKey = request => {
|
||||
|
||||
+101
-1
@@ -818,6 +818,105 @@ new Vue({
|
||||
navigator.clipboard.readText().then(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: {
|
||||
@@ -860,11 +959,12 @@ new Vue({
|
||||
this.disclaimerDialog.show = true
|
||||
this.$q.localStorage.set('lnbits.disclaimerShown', true)
|
||||
}
|
||||
|
||||
// listen to incoming payments
|
||||
LNbits.events.onInvoicePaid(this.g.wallet, payment =>
|
||||
this.onPaymentReceived(payment.payment_hash)
|
||||
)
|
||||
eventReactionWebocket(wallet.id)
|
||||
this.checkFirstUse()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"js/components.js",
|
||||
"js/components/lnbits-funding-sources.js",
|
||||
"js/components/extension-settings.js",
|
||||
"js/event-reactions.js",
|
||||
"js/bolt11-decoder.js"
|
||||
],
|
||||
"css": ["vendor/quasar.css", "vendor/Chart.css", "css/base.css"]
|
||||
|
||||
@@ -14,11 +14,9 @@ from pywebpush import WebPushException, webpush
|
||||
from lnbits.core.crud import (
|
||||
delete_expired_invoices,
|
||||
delete_webpush_subscriptions,
|
||||
get_balance_checks,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
)
|
||||
from lnbits.core.services import redeem_lnurl_withdraw
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
|
||||
@@ -186,14 +184,6 @@ async def check_pending_payments():
|
||||
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):
|
||||
"""
|
||||
Takes incoming payments, sets pending=False, and dispatches them to
|
||||
|
||||
@@ -168,13 +168,17 @@
|
||||
show-if-above
|
||||
:elevated="$q.screen.lt.md"
|
||||
>
|
||||
<lnbits-wallet-list></lnbits-wallet-list>
|
||||
<lnbits-wallet-list id="wallet-list"></lnbits-wallet-list>
|
||||
|
||||
<lnbits-manage
|
||||
id="admin-manage"
|
||||
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
||||
:show-node="'{{LNBITS_NODE_UI}}' == 'True'"
|
||||
></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>
|
||||
{% endblock %} {% block page_container %}
|
||||
<q-page-container>
|
||||
|
||||
Generated
+345
-755
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,7 @@
|
||||
"js/components.js",
|
||||
"js/components/lnbits-funding-sources.js",
|
||||
"js/components/extension-settings.js",
|
||||
"js/event-reactions.js",
|
||||
"js/bolt11-decoder.js"
|
||||
],
|
||||
"css": [
|
||||
|
||||
Generated
+1
-1
@@ -2862,4 +2862,4 @@ liquid = ["wallycore"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10 | ^3.9"
|
||||
content-hash = "c29cac992fae552eb8da791e13fe7766eab7571e260a42cc0de0d755508c42b7"
|
||||
content-hash = "b2e21e0075047150888581a401d46fbe8efd6226e85a85189c3f3db51f825a48"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "lnbits"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ is_regtest: bool = not is_fake
|
||||
docker_lightning_cli = [
|
||||
"docker",
|
||||
"exec",
|
||||
"lnbits-legend-lnd-1-1",
|
||||
"lnbits-lnd-1-1",
|
||||
"lncli",
|
||||
"--network",
|
||||
"regtest",
|
||||
@@ -46,7 +46,7 @@ docker_lightning_cli = [
|
||||
docker_bitcoin_cli = [
|
||||
"docker",
|
||||
"exec",
|
||||
"lnbits-legend-bitcoind-1-1" "bitcoin-cli",
|
||||
"lnbits-bitcoind-1-1" "bitcoin-cli",
|
||||
"-rpcuser=lnbits",
|
||||
"-rpcpassword=lnbits",
|
||||
"-regtest",
|
||||
@@ -56,7 +56,7 @@ docker_bitcoin_cli = [
|
||||
docker_lightning_unconnected_cli = [
|
||||
"docker",
|
||||
"exec",
|
||||
"lnbits-legend-lnd-2-1",
|
||||
"lnbits-lnd-2-1",
|
||||
"lncli",
|
||||
"--network",
|
||||
"regtest",
|
||||
|
||||
Reference in New Issue
Block a user